diff --git a/.codex/skills/beat-saber-mapping/scripts/choreography.py b/.codex/skills/beat-saber-mapping/scripts/choreography.py index 9e14a45..0e8de44 100644 --- a/.codex/skills/beat-saber-mapping/scripts/choreography.py +++ b/.codex/skills/beat-saber-mapping/scripts/choreography.py @@ -25,6 +25,7 @@ ) from kinematics import bomb_on_swing_path, double_is_safe, swing_pose, transition_finding from official_corpus import map_features, normalize_beatmap +from mapping_plan import build_section_plan, normalize_mapping_plan from safety_contract import ( RECOVERY_BEATS, hold_span_beats, @@ -184,6 +185,8 @@ def select_intents(analysis: dict[str, Any], sections: dict[str, Any], difficult ) gap = config.peak_min_gap if is_peak else config.min_gap adaptive_gap = max(gap, gap * (1.15 - 0.35 * intensity)) + density = float(section.get("density", 1.0)) + adaptive_gap = max(gap, adaptive_gap / max(0.35, density)) if beat - last_beat + 1e-9 < adaptive_gap: continue if any( @@ -194,13 +197,16 @@ def select_intents(analysis: dict[str, Any], sections: dict[str, Any], difficult continue strength = float(event.get("strength", 0.0)) energies = event.get("stemEnergy") if isinstance(event.get("stemEnergy"), dict) else {} + dominant = str(section.get("dominantInstrument", "auto")) + if dominant != "auto" and dominant in energies: + strength *= 0.7 + 1.1 * max(0.0, min(1.0, float(energies[dominant]))) layer = str(event.get("layer") or "") instrument = float(energies.get("drums") or 0.0) + float(energies.get("bass") or 0.0) + float(energies.get("guitar") or 0.0) vocal = float(energies.get("vocals") or 0.0) layered = bool(energies) and instrument >= vocal * 1.15 and layer in {"drums", "bass", "guitar", "piano", "other"} if difficulty in {"Expert", "ExpertPlus"} and layered: strength *= 1.35 - threshold = base_threshold + threshold = base_threshold / max(0.35, density) if is_peak: threshold *= 0.88 if difficulty in {"Expert", "ExpertPlus"} else 0.94 elif intensity < 0.4: @@ -256,6 +262,15 @@ def select_intents(analysis: dict[str, Any], sections: dict[str, Any], difficult "layer": layer or "mix", "stemEnergy": energies, } + if "motifId" in section: + relative = beat - float(section.get("startBeat", 0.0)) + motif_offset = int(section.get("motifOffset", 0)) + payload.update({ + "sectionId": section["id"], "motifId": section["motifId"], + "patternBeat": relative + motif_offset * 4.0, + "patternIndex": int(round(relative * 2.0)) + motif_offset, + "style": section.get("style", "balanced"), + }) selected.append(payload) last_beat = beat return selected @@ -433,6 +448,7 @@ def _candidate_notes( index: int, intensity: float, beat: float, + preference_beat: float | None = None, ) -> list[dict[str, Any]]: """Enumerate combo-first arrow poses. Dots are a rare reset, not the default cut.""" @@ -440,8 +456,9 @@ def _candidate_notes( home = (0, 1) else: home = (3, 2) - preferred = swing_family(color, parity, beat) - lead = cut_for(color, parity, index, beat) + phase = beat if preference_beat is None else preference_beat + preferred = swing_family(color, parity, phase) + lead = cut_for(color, parity, index, phase) notes: list[dict[str, Any]] = [] seen: set[tuple[int, int, int]] = set() @@ -627,9 +644,14 @@ def _pose_domain( intensity = float(intent.get("intensity", 0.5)) kind = intent["kind"] duration = float(intent.get("duration") or 0.0) - heads = _candidate_notes(color, parity, index, intensity, beat) + index = int(intent.get("patternIndex", index)) + int(intent.get("candidateVariant", 0)) + phase = float(intent.get("patternBeat", beat)) + heads = _candidate_notes(color, parity, index, intensity, beat, preference_beat=phase) + if intent.get("style") in {"flow", "chill"}: + home_x = 0.5 if color == 0 else 2.5 + heads.sort(key=lambda head: abs(float(head["x"]) - home_x) + (0.5 if int(head["d"]) in {2, 3, 8} else 0.0)) if str(intent.get("layer") or "") == "combo-stack": - lead = cut_for(color, parity, index, beat) + lead = cut_for(color, parity, index, phase) if color == 0: pair_cells = ((1, 1), (0, 1), (1, 0), (1, 2)) else: @@ -667,7 +689,9 @@ def _joint_combined_domain( beat = float(intent["beat"]) stack = str(intent.get("layer") or "") == "combo-stack" - lead_parity = int(beat // 4.0) % 2 if stack else (index + int(beat // 2.0)) % 2 + pattern_beat = float(intent.get("patternBeat", beat)) + pattern_index = int(intent.get("patternIndex", index)) + int(intent.get("candidateVariant", 0)) + lead_parity = int(pattern_beat // 4.0) % 2 if stack else (pattern_index + int(pattern_beat // 2.0)) % 2 buckets = { (color_id, parity): _pose_domain(intent, color_id, parity, index, bpm, recovery_beats)[:6] for color_id in (0, 1) @@ -2500,12 +2524,13 @@ def score_candidate(database_path: Path | None, features: dict[str, Any]) -> dic } -def generate_all( +def _generate_single_pass( analysis: dict[str, Any], sections: dict[str, Any], seed: int, corpus_database: Path | None = None, difficulties: Sequence[str] | None = None, + candidate_variant: int = 0, ) -> tuple[dict[str, dict[str, Any]], dict[str, Any]]: generated: dict[str, dict[str, Any]] = {} report: dict[str, Any] = {"solver": {}, "difficulties": {}, "officialReferences": {}} @@ -2513,6 +2538,8 @@ def generate_all( for difficulty in chosen: emit_progress("choreography", f"selecting {difficulty} intents from onsets and sections") intents = select_intents(analysis, sections, difficulty) + if candidate_variant: + intents = [{**intent, "candidateVariant": candidate_variant} for intent in intents] joint = None try: emit_progress("choreography", f"solving joint CP-SAT for {difficulty} ({len(intents)} events)") @@ -2559,3 +2586,97 @@ def generate_all( ) report["difficulties"][difficulty]["corpusRank"] = score_candidate(corpus_database, candidate_features) return generated, report + + +def rank_choreography_candidate( + beatmap: dict[str, Any], difficulty: str, bpm: float, + corpus_rank: dict[str, Any], reference: dict[str, Any], + expected_count: int, +) -> dict[str, Any]: + """Score complete candidates only after running the unchanged structural gates.""" + from beatforge_core import ValidationReport + from validate_map import validate_v3 + + validation = ValidationReport() + validate_v3(beatmap, f"{difficulty}Standard.dat", validation, bpm=bpm, difficulty=difficulty) + notes = beatmap.get("colorNotes", []) + errors = [issue.code for issue in validation.errors] + if expected_count and not notes: + errors.append("NO_PLAYABLE_NOTES") + events, _ = normalize_beatmap(beatmap) + features = map_features(events, bpm) + profile = reference.get("profile", {}) + comparable = [name for name in ("p95Reach", "centerVisionRate", "medianSameHandGap") if name in profile] + profile_distance = sum(abs(float(features.get(name, 0.0)) - float(profile[name])) / max(0.25, abs(float(profile[name]))) for name in comparable) / max(1, len(comparable)) + reference_score = 1.0 / (1.0 + profile_distance) if comparable else 0.5 + corpus_score = float(corpus_rank.get("score", 50.0)) / 100.0 + coverage = min(1.0, len(notes) / max(1, expected_count)) + signatures = [(int(note["x"]), int(note["y"]), int(note["d"])) for note in notes] + repeats = sum(left == right for left, right in zip(signatures, signatures[1:])) + variety = 1.0 - repeats / max(1, len(signatures) - 1) + score = 100.0 * (0.4 * corpus_score + 0.25 * reference_score + 0.25 * coverage + 0.1 * variety) + return { + "score": round(score, 6), "hardErrors": sorted(set(errors)), + "eligible": not errors, "corpusScore": round(corpus_score * 100.0, 6), + "referenceScore": round(reference_score * 100.0, 6), + "eventCoverage": round(coverage, 6), "variety": round(variety, 6), + "features": features, + } + + +def select_ranked_candidate(candidates: list[dict[str, Any]]) -> dict[str, Any]: + eligible = [candidate for candidate in candidates if candidate.get("eligible")] + if not eligible: + codes = sorted({code for candidate in candidates for code in candidate.get("hardErrors", [])}) + raise ValueError("No candidate passed the unchanged safety gates: " + ", ".join(codes)) + return max(eligible, key=lambda candidate: (candidate["score"], -candidate["index"])) + + +def generate_all( + analysis: dict[str, Any], sections: dict[str, Any], seed: int, + corpus_database: Path | None = None, + difficulties: Sequence[str] | None = None, + mapping_plan: dict[str, Any] | None = None, +) -> tuple[dict[str, dict[str, Any]], dict[str, Any]]: + if mapping_plan is None: + return _generate_single_pass(analysis, sections, seed, corpus_database, difficulties) + controls = normalize_mapping_plan(mapping_plan) + planned = build_section_plan(analysis, sections, controls) + chosen = tuple(name for name in DIFFICULTIES if name in set(difficulties or DIFFICULTIES)) or DIFFICULTIES + generated: dict[str, dict[str, Any]] = {} + report: dict[str, Any] = {"solver": {}, "difficulties": {}, "officialReferences": {}, "candidateRanking": {}, "mappingPlan": planned} + for difficulty in chosen: + # Retrieve before generation so every candidate is compared with the same reference family. + reference = retrieve_official_references(corpus_database, difficulty, float(analysis["bpm"])) + intents = select_intents(analysis, planned, difficulty) + candidates = [] + payloads: dict[int, tuple[dict[str, Any], dict[str, Any]]] = {} + for index in range(controls["candidateCount"]): + emit_progress("choreography", f"{difficulty}: generating candidate {index + 1}/{controls['candidateCount']}") + try: + maps, trace = _generate_single_pass(analysis, planned, seed + index * 7919, corpus_database, (difficulty,), candidate_variant=index) + beatmap = maps[difficulty] + if controls["noBombs"]: + beatmap["bombNotes"] = [] + if controls["noWalls"]: + beatmap["obstacles"] = [] + ranked_map = beatmap + if len(analysis.get("beatGrid", [])) >= 2: + from timing_export import project_map_timing + ranked_map = project_map_timing(beatmap, analysis) + ranked_events, _ = normalize_beatmap(ranked_map) + corpus_rank = score_candidate(corpus_database, map_features(ranked_events, float(analysis["bpm"]))) + rank = rank_choreography_candidate(ranked_map, difficulty, float(analysis["bpm"]), corpus_rank, reference, len(intents)) + trace["difficulties"][difficulty]["corpusRank"] = corpus_rank + candidates.append({"index": index, **rank}) + payloads[index] = (beatmap, trace) + except (RuntimeError, ValueError) as error: + candidates.append({"index": index, "eligible": False, "score": 0.0, "hardErrors": [str(error)]}) + winner = select_ranked_candidate(candidates) + beatmap, trace = payloads[winner["index"]] + generated[difficulty] = beatmap + report["solver"][difficulty] = trace["solver"][difficulty] + report["difficulties"][difficulty] = trace["difficulties"][difficulty] + report["officialReferences"][difficulty] = reference + report["candidateRanking"][difficulty] = {"selectedIndex": winner["index"], "candidates": candidates} + return generated, report diff --git a/.codex/skills/beat-saber-mapping/scripts/generate_map.py b/.codex/skills/beat-saber-mapping/scripts/generate_map.py index 81d79be..f189144 100644 --- a/.codex/skills/beat-saber-mapping/scripts/generate_map.py +++ b/.codex/skills/beat-saber-mapping/scripts/generate_map.py @@ -17,6 +17,8 @@ from beatforge_core import DIFFICULTIES, TOOL_VERSION, default_cache_dir, difficulty_rank, emit_progress, encode_ogg, find_ffmpeg, load_json, parse_difficulties, sha256_file, write_json from choreography import CONFIGS, generate_all +from mapping_plan import build_section_plan, normalize_mapping_plan +from timing_export import project_map_timing, project_plan_timing from artwork import derive_palette, extract_embedded_artwork, lookup_itunes_cover, lookup_release_cover, palette_from_rgb, write_palette_cover from official_corpus import corpus_is_fresh, detect_game_root, sync as sync_official_corpus @@ -373,6 +375,7 @@ def main() -> int: parser.add_argument("--profile", default="official-premium", choices=["official-premium", "official-rl"]) parser.add_argument("--engine", default="cp-sat", choices=["cp-sat", "rl", "hybrid"], help="Choreography engine to use (cp-sat, rl, hybrid)") parser.add_argument("--rl-model", type=Path, help="Path to trained PyTorch RL policy checkpoint") + parser.add_argument("--mapping-plan", type=Path, help="Version 1 structured creative controls JSON") parser.add_argument("--environment", default=None, help="Beat Saber environment name (e.g. DaftPunkEnvironment, DefaultEnvironment)") parser.add_argument("--full-spread", action="store_true", default=True) parser.add_argument( @@ -393,6 +396,13 @@ def main() -> int: parser.add_argument("--game-root", type=Path, help="Beat Saber install root; auto-detected on common Oculus/Steam paths") parser.add_argument("--seed", type=int) args = parser.parse_args() + try: + creative_controls = normalize_mapping_plan(load_json(args.mapping_plan)) if args.mapping_plan else None + if (args.engine == "rl" or args.profile == "official-rl") and (args.rl_model is None or not args.rl_model.is_file()): + raise ValueError("The RL engine requires an existing trained checkpoint supplied with --rl-model") + except (ValueError, OSError) as error: + print(f"ERROR: {error}", file=sys.stderr) + return 2 if not args.audio.is_file(): print(f"ERROR: audio file does not exist: {args.audio}", file=sys.stderr) return 2 @@ -423,6 +433,7 @@ def main() -> int: emit_progress("timing", "reusing existing analysis.json") analysis = load_json(analysis_file) sections = load_json(sections_file) + creative_plan = build_section_plan(analysis, sections, creative_controls) if creative_controls is not None else None unconfirmed = bool(args.continue_unconfirmed) if analysis.get("status") != "timing_verified": if not unconfirmed: @@ -496,54 +507,33 @@ def main() -> int: emit_progress("choreography", f"solving {', '.join(chosen)}") if args.engine == "rl" or args.profile == "official-rl": from rl.policy_generator import RLMapGenerator + from rl.features import load_analysis_features rl_gen = RLMapGenerator(model_path=args.rl_model) - raw_bg = load_json(analysis_dir / "beat_grid.json") if (analysis_dir / "beat_grid.json").exists() else None - if isinstance(raw_bg, dict) and "tempoRegions" in raw_bg: - max_beat = max((float(r.get("endBeat", 0.0)) for r in raw_bg.get("tempoRegions", [])), default=0.0) - beat_grid = [round(i * 0.25, 4) for i in range(max(16, int(max_beat * 4)))] - elif isinstance(raw_bg, list): - beat_grid = [float(b) for b in raw_bg] - else: - duration_s = float(analysis.get("durationSeconds", analysis.get("duration", 240.0))) - bpm_val = float(analysis.get("bpm", 125.0)) - beat_grid = [round(i * 0.25, 4) for i in range(max(16, int(duration_s * bpm_val / 60.0 * 4)))] - - grid_len = len(beat_grid) - try: - import numpy as np - from beatforge_core import frame_features, load_audio - audio_buf = load_audio(args.audio) - feat = frame_features(audio_buf.samples) - flux = feat["flux"] - onsets = np.interp(np.linspace(0, len(flux), grid_len), np.arange(len(flux)), flux) - onsets = ((onsets - np.min(onsets)) / (np.max(onsets) - np.min(onsets) + 1e-6)).tolist() - except Exception: - onsets = [0.5] * grid_len - - audio_features = { - "onsets": onsets, - "flux": onsets, - "stems": { - "drums": onsets, - "bass": onsets, - "vocals": [o * 0.7 for o in onsets], - "guitar": [o * 0.5 for o in onsets], - "piano": [o * 0.5 for o in onsets], - "other": [o * 0.4 for o in onsets], - }, - "sections": [s.get("type", "verse") for s in sections] if isinstance(sections, list) else ["verse"] * grid_len, - } + bundle = load_analysis_features(analysis_dir, require_verified=not unconfirmed) maps = {} for diff in chosen: - maps[diff] = rl_gen.generate_difficulty(audio_features, beat_grid, float(analysis["bpm"]), difficulty=diff) + maps[diff] = rl_gen.generate_difficulty(bundle.audio_features, bundle.beat_grid, bundle.bpm, difficulty=diff, seed=seed) + if creative_controls and creative_controls["noBombs"]: + maps[diff]["bombNotes"] = [] + if creative_controls and creative_controls["noWalls"]: + maps[diff]["obstacles"] = [] choreography_report = { "poseSolver": "rl-policy-v1", "solver": "rl-policy-v1", "difficulties": chosen, + "checkpoint": rl_gen.checkpoint_metadata, + "features": bundle.provenance, + "mappingPlan": creative_plan, + "creativeControlSupport": "RL applies noBombs and noWalls only; phrase, density, and style controls require the premium engine", } else: - maps, choreography_report = generate_all(analysis, sections, seed, args.corpus_database, difficulties=chosen) + maps, choreography_report = generate_all(analysis, sections, seed, args.corpus_database, difficulties=chosen, mapping_plan=creative_controls) + maps = {difficulty: project_map_timing(payload, analysis) for difficulty, payload in maps.items()} + if creative_plan is not None: + creative_plan = project_plan_timing(creative_plan, analysis) + choreography_report["mappingPlan"] = creative_plan + write_json(package_analysis_dir / "mapping_plan.json", creative_plan) for difficulty, payload in maps.items(): write_json(args.out / f"{difficulty}Standard.dat", payload) emit_progress("audio", "encoding song.ogg for the Beat Saber pack") @@ -590,6 +580,7 @@ def main() -> int: }, }, "choreography": choreography_report, + "mappingPlan": creative_plan, "officialCorpus": {"database": str(args.corpus_database.resolve()), **corpus_report}, "documentation": { "manifest": "_beatforge/documentation_manifest.json", diff --git a/.codex/skills/beat-saber-mapping/scripts/mapping_plan.py b/.codex/skills/beat-saber-mapping/scripts/mapping_plan.py new file mode 100644 index 0000000..7e4a0bb --- /dev/null +++ b/.codex/skills/beat-saber-mapping/scripts/mapping_plan.py @@ -0,0 +1,133 @@ +"""Versioned, deterministic creative controls shared by Studio and the portable mapper.""" + +from __future__ import annotations + +import math +import re +from typing import Any + +SCHEMA_VERSION = 1 +INSTRUMENTS = ("auto", "drums", "bass", "vocals", "guitar", "piano", "other") +STYLES = ("balanced", "flow", "tech", "chill") +SCALE_FIELDS = ("intensity", "density", "verseDensity", "chorusDensity") +DEFAULTS = { + "schemaVersion": SCHEMA_VERSION, "brief": "", "intensity": 1.0, + "density": 1.0, "verseDensity": 1.0, "chorusDensity": 1.0, + "dominantInstrument": "auto", "style": "balanced", "noBombs": False, + "noWalls": False, "candidateCount": 2, "sectionOverrides": [], +} + + +def _scale(value: Any, field: str) -> float: + if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or not 0.5 <= value <= 1.5: + raise ValueError(f"{field} must be a number from 0.5 to 1.5") + return float(value) + + +def normalize_mapping_plan(value: dict[str, Any] | None = None) -> dict[str, Any]: + """Resolve supported brief phrases, with explicit controls taking precedence.""" + if value is None: + value = {} + if not isinstance(value, dict): + raise ValueError("mapping plan must be an object") + unknown = set(value) - set(DEFAULTS) - {"interpretation"} + if unknown: + raise ValueError("unknown mapping plan controls: " + ", ".join(sorted(unknown))) + if type(value.get("schemaVersion", 1)) is not int or value.get("schemaVersion", 1) != 1: + raise ValueError("mapping plan schemaVersion must be 1") + brief = value.get("brief", "") + if not isinstance(brief, str) or len(brief) > 2000: + raise ValueError("brief must be text of at most 2000 characters") + text = brief.casefold() + inferred: dict[str, Any] = {} + recognized: list[str] = [] + phrases = [ + (r"\b(?:no|without) bombs\b", "noBombs", True, "No bombs"), + (r"\b(?:no|without) walls\b", "noWalls", True, "No walls"), + (r"\b(?:sparse|lighter|easier) verses?\b", "verseDensity", 0.75, "Lighter verses"), + (r"\b(?:dense|denser|bigger|stronger) chorus(?:es)?\b", "chorusDensity", 1.25, "Denser choruses"), + (r"\b(?:less|fewer) notes\b", "density", 0.75, "Fewer notes"), + (r"\bmore notes\b", "density", 1.25, "More notes"), + (r"\b(?:relaxed|chill)\b", "style", "chill", "Chill style"), + (r"\b(?:flow|flowing|smooth)\b", "style", "flow", "Flow style"), + (r"\b(?:tech|technical)\b", "style", "tech", "Tech style"), + ] + for pattern, field, setting, description in phrases: + if re.search(pattern, text): + inferred[field] = setting + if field not in value: + recognized.append(description) + for instrument in INSTRUMENTS[1:]: + if re.search(r"\b(?:follow|focus on|emphasize) (?:the )?" + instrument + r"\b", text): + inferred["dominantInstrument"] = instrument + if "dominantInstrument" not in value: + recognized.append("Follow " + instrument) + result = {**DEFAULTS, **inferred, **value, "brief": brief} + for field in SCALE_FIELDS: + result[field] = _scale(result[field], field) + if result["dominantInstrument"] not in INSTRUMENTS: + raise ValueError("unknown dominantInstrument") + if result["style"] not in STYLES: + raise ValueError("unknown mapping style") + for field in ("noBombs", "noWalls"): + if not isinstance(result[field], bool): + raise ValueError(f"{field} must be a boolean") + count = result["candidateCount"] + if isinstance(count, bool) or not isinstance(count, int) or not 1 <= count <= 3: + raise ValueError("candidateCount must be an integer from 1 to 3") + overrides = result["sectionOverrides"] + if not isinstance(overrides, list) or len(overrides) > 200: + raise ValueError("sectionOverrides must be a list with at most 200 entries") + checked = [] + seen = set() + for override in overrides: + if not isinstance(override, dict) or set(override) - {"id", "density", "intensity", "dominantInstrument"}: + raise ValueError("invalid section override") + section_id = override.get("id") + if not isinstance(section_id, str) or not section_id or len(section_id) > 100 or section_id in seen: + raise ValueError("section overrides need unique nonempty ids") + seen.add(section_id) + item = dict(override) + for field in ("density", "intensity"): + if field in item: + item[field] = _scale(item[field], "section " + field) + if "dominantInstrument" in item and item["dominantInstrument"] not in INSTRUMENTS: + raise ValueError("unknown section dominantInstrument") + checked.append(item) + result["sectionOverrides"] = checked + interpretation = value.get("interpretation", []) + if not isinstance(interpretation, list) or not all(isinstance(item, str) for item in interpretation): + raise ValueError("interpretation must be a list of phrases") + result["interpretation"] = recognized or list(interpretation) + return result + + +def build_section_plan(analysis: dict[str, Any], sections: dict[str, Any], value: dict[str, Any]) -> dict[str, Any]: + plan = normalize_mapping_plan(value) + source = sections.get("sections") or [{"label": "body", "startBeat": 0.0, "endBeat": float(analysis.get("durationSeconds", 0.0)) * float(analysis.get("bpm", 120.0)) / 60.0, "intensity": 0.5}] + overrides = {entry["id"]: entry for entry in plan["sectionOverrides"]} + style_scale = {"balanced": 1.0, "flow": 0.95, "tech": 1.05, "chill": 0.75}[plan["style"]] + planned = [] + for index, section in enumerate(source): + item = dict(section) + section_id = str(item.get("id") or f"section-{index:03d}") + label = str(item.get("label") or item.get("type") or "body") + family = re.sub(r"[\d\W_]+", "", label.casefold()) or "body" + chorus = any(word in family for word in ("chorus", "drop", "peak")) + verse = "verse" in family + factor = plan["chorusDensity"] if chorus else plan["verseDensity"] if verse else 1.0 + override = overrides.get(section_id, {}) + density = plan["density"] * factor * style_scale * override.get("density", 1.0) + item.update({ + "id": section_id, "label": label, "motifId": family, + "motifOffset": sum((i + 1) * ord(character) for i, character in enumerate(family)) % 4, + "density": round(max(0.35, min(1.75, density)), 6), + "intensity": max(0.0, min(1.0, float(item.get("intensity", 0.5)) * plan["intensity"] * override.get("intensity", 1.0))), + "dominantInstrument": override.get("dominantInstrument", plan["dominantInstrument"]), + "style": plan["style"], + }) + planned.append(item) + missing = set(overrides) - {item["id"] for item in planned} + if missing: + raise ValueError("unknown section ids: " + ", ".join(sorted(missing))) + return {"schemaVersion": 1, "source": sections.get("source", "internal"), "sections": planned, "controls": plan} diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/README.md b/.codex/skills/beat-saber-mapping/scripts/rl/README.md new file mode 100644 index 0000000..2b121cf --- /dev/null +++ b/.codex/skills/beat-saber-mapping/scripts/rl/README.md @@ -0,0 +1,148 @@ +# RL training, evaluation, and checkpoint promotion + +RL remains experimental. The studio's constraint solver is the default. A saved +PPO reward is not proof of musical quality, and generated maps still require the +normal package validator and human headset checks. + +Set `PYTHONPATH` to `skills/beat-saber-mapping/scripts` and use the project's Python +environment. PyTorch is an optional dependency. These commands operate on local +files and do not download models, analyze new audio, or install songs. + +## Prepare a song library + +Run `analyze_audio.py` and resolve the normal timing gate first. Every song needs +`analysis.json`, `beat_grid.json`, `sections.json`, and `audio_features.npz`. +Training and evaluation require `timing_verified`. The studio's explicit +unconfirmed-generation override is the only caller that disables this check, +and its feature provenance continues to say `needs_anchors`. + +Use a manifest outside the repository for your private audio library: + +```json +{ + "schemaVersion": 1, + "tracks": [ + {"id": "training-song", "split": "train", "analysisDir": "analysis/training-song"}, + {"id": "validation-song", "split": "validation", "analysisDir": "analysis/validation-song"}, + {"id": "final-test-song", "split": "test", "analysisDir": "analysis/final-test-song"} + ] +} +``` + +Paths resolve relative to the manifest. IDs and analyzed audio SHA-256 hashes +must be unique across the entire library. Use varied songs and keep the final +test split untouched while selecting models. Audio, analysis, maps, checkpoints, +and evaluation outputs stay out of version control. + +## Train with the same inputs used at generation + +```text +python -m rl.train_custom_tracks --manifest library.json --rounds 1 --timesteps-per-track 16384 --seed 42 --out data/models/candidate.pt +python -m rl.train_ppo --analysis-dir analysis/song --timesteps 16384 --seed 42 --out data/models/single-song.pt +``` + +`--analysis-dir` can be repeated for library training without a manifest. +`--no-resume` explicitly starts fresh. Resuming a real-song run from a checkpoint +without recorded training hashes is rejected because held-out independence +cannot be established. Old `--ram-dir`, `--ninajirachi-dir`, and `--spotify-dir` +discovery was replaced by explicit artifact directories or a manifest. + +The training loop uses the requested number of transitions exactly. Full song +duration stays in the environment; a short requested training budget may still +cover only part of a song. The default is one library round. It no longer silently +performs 99 rounds. Seeds cover initialization, policy sampling, and batch order. +Repeatability applies to the same device and runtime versions. + +The second hand's action mask uses the first hand's selected note. Simultaneous +handclaps, intersecting saber paths, and center vision doubles are masked during +PPO collection and inference. PPO stores that conditional mask for replay, so +the action probability used during optimization matches collection. This mask +does not replace validation of holds, walls, bombs, and the final package. + +For a software smoke test, use the explicit `--synthetic` option on `train_ppo`. +That pulse has no separated stems or verified song timing and cannot establish +held-out song performance. + +`rl.features.load_analysis_features(path)` returns `AnalysisFeatures` with +`audio_features`, `beat_grid`, `bpm`, `provenance`, and the original `analysis`. +The schema is `beatforge-rl-sample-grid-v2`. Quarter-beat decision samples come +from the monotonic integer sample grid, including offsets and tempo changes. +Frame interpolation uses saved sample positions, seconds, or `hopSamples`. +`sections.json.sections` supplies interval labels. Sample and second boundaries +take precedence over derived beat fields. Real Demucs event `stemEnergy` +annotations enter their nearest decision step. Missing stems remain zero and +`stemAvailability` records which stems actually exist. Missing spectral flux is +zero with `fluxAvailable: false`; the mix onset envelope is never relabeled as a +separated instrument. The 69-value policy observation layout remains compatible; +availability flags and provenance are metadata, while section labels influence +the musical reward. + +Checkpoints contain `state_dict` and JSON-compatible `metadata`, including the +feature schema, seed, training audio hashes, and analysis provenance. Saves use +an atomic replacement. Inference uses `weights_only=True`, checks tensor shapes +and finite values, and records the exact checkpoint SHA-256. Legacy raw state +dicts can still be loaded explicitly for generation with +`featureSchema: legacy-unrecorded`. They cannot enter a verified held-out +comparison. A missing or incompatible checkpoint never falls back to a random +policy. Callers supplying a policy instance directly are marked +`source: supplied-policy`, for tests or explicit application integration. + +## Compare on held-out songs + +```text +python -m rl.evaluate --manifest library.json --checkpoint data/models/baseline.pt --checkpoint data/models/candidate.pt --split validation --difficulty Expert --seed 42 --out data/evaluations/comparison.json +``` + +The command checks checkpoint training hashes against held-out audio hashes, +generates each chart deterministically, projects it onto the exact audio clock, +and runs the shared chart validator. It reports hard errors, warning codes, +NPS, hand imbalance, repeated transitions, section density, and the mean onset +strength at note positions. Empty charts count as failures. The report includes +sample artifact hashes, checkpoint hashes, evaluator code hashes, runtime +versions, chart hashes, and the seed. Exit status 2 means a generated chart has +hard errors; the JSON report is still saved. + +These measurements do not replace packaged-map checks or human evaluation. +Blind preferences, manual corrections, and headset playtests are separate +evidence. No generated report claims those checks happened. + +## Promote and roll back + +```text +python -m rl.registry --registry data/model_registry initialize --checkpoint data/models/baseline.pt --evaluation data/evaluations/comparison.json +python -m rl.registry --registry data/model_registry promote --checkpoint data/models/candidate.pt --evaluation data/evaluations/comparison.json +python -m rl.registry --registry data/model_registry active +python -m rl.registry --registry data/model_registry rollback +``` + +Initialization explicitly selects a validated, nonempty baseline. Promotion +requires identical manifest, split, song analysis hashes, difficulty, seed, +evaluator code, feature schema, and runtime versions. Each candidate chart must +have zero hard errors and no increase in warnings, hand imbalance, repeated +transition fraction, or distance from the difficulty's target NPS. No held-out +song may regress in onset alignment, and the mean onset-strength gain must be +at least 0.001. This is a measured candidate improvement, not proof that humans +prefer it. If the benchmark or evaluator changes, evaluate both models again +and explicitly establish a new registry baseline. + +The registry copies checkpoints and evaluations into folders named by their +content hashes. `registry.json` contains `schemaVersion`, `active`, `models`, +and an append-only `history` of selections. Promotion atomically switches the +active reference and retains prior files for rollback. It does not delete or +overwrite the source checkpoints. Concurrent writes fail with a lock error. +`active_checkpoint(registry_root)` verifies the active file's hash before +returning its path. A feedback-triggered local job can call +`register_checkpoint(root, checkpoint, evaluation)` after an explicitly run +training/evaluation cycle; this module starts no background work. + +## Focused checks + +```text +python -m pytest skills/beat-saber-mapping/tests/test_rl_features.py skills/beat-saber-mapping/tests/test_rl_training_correctness.py skills/beat-saber-mapping/tests/test_rl_registry.py skills/beat-saber-mapping/tests/test_rl_environment.py +``` + +Tests cover offset and tempo-change sampling, localized real stems, missing +inputs, section intervals, train/eval content leakage, checkpoint corruption, +terminal PPO bootstrap boundaries, exact budgets, seeds, promotion rejection, +and artifact-preserving rollback. They use small synthetic fixtures rather than +training on or redistributing private song audio. diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/__init__.py b/.codex/skills/beat-saber-mapping/scripts/rl/__init__.py index b1b74ac..5a22b08 100644 --- a/.codex/skills/beat-saber-mapping/scripts/rl/__init__.py +++ b/.codex/skills/beat-saber-mapping/scripts/rl/__init__.py @@ -2,9 +2,18 @@ from __future__ import annotations -from .environment import BeatSaberEnv -from .models import ActorCriticPolicy, BeatForgeAudioEncoder -from .rewards import CompositeReward, KinematicReward, MusicalReward, StyleReward +def __getattr__(name): + # Feature extraction and benchmark metric helpers work without PyTorch. + if name in {"ActorCriticPolicy", "BeatForgeAudioEncoder"}: + from . import models + return getattr(models, name) + if name == "BeatSaberEnv": + from .environment import BeatSaberEnv + return BeatSaberEnv + if name in {"CompositeReward", "KinematicReward", "MusicalReward", "StyleReward"}: + from . import rewards + return getattr(rewards, name) + raise AttributeError(name) __all__ = [ "BeatSaberEnv", diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/checkpoints.py b/.codex/skills/beat-saber-mapping/scripts/rl/checkpoints.py new file mode 100644 index 0000000..bb3fe62 --- /dev/null +++ b/.codex/skills/beat-saber-mapping/scripts/rl/checkpoints.py @@ -0,0 +1,58 @@ +"""Validated policy weights and portable, non-pickle provenance metadata.""" + +from __future__ import annotations + +from pathlib import Path +import json +from typing import Any + +import torch + +from .features import FEATURE_SCHEMA, file_sha256 +from .models import ActorCriticPolicy + + +def load_checkpoint(path: Path, device: str = "cpu") -> tuple[ActorCriticPolicy, dict[str, Any]]: + path = Path(path) + if not path.is_file(): + raise FileNotFoundError(f"RL checkpoint does not exist: {path}") + try: + payload = torch.load(path, map_location=device, weights_only=True) + if not isinstance(payload, dict): + raise ValueError("checkpoint must contain policy weights") + wrapped = "state_dict" in payload + weights = payload["state_dict"] if wrapped else payload + metadata = dict(payload.get("metadata", {})) if wrapped else {"featureSchema": "legacy-unrecorded"} + json.dumps(metadata, allow_nan=False) + for name in ("trainingSourceSha256", "heldOutSourceSha256"): + if name in metadata: + hashes = metadata[name] + if (not isinstance(hashes, list) or not all(isinstance(value, str) and len(value) == 64 + and all(character in "0123456789abcdef" for character in value) for value in hashes)): + raise ValueError(f"Invalid checkpoint lineage field: {name}") + schema = metadata.get("featureSchema", "legacy-unrecorded") + if schema not in {FEATURE_SCHEMA, "legacy-unrecorded"}: + raise ValueError(f"unsupported feature schema: {schema}") + first = weights["encoder.0.weight"] + if first.ndim != 2 or first.shape[1] != 69: + raise ValueError("checkpoint observation dimensions are incompatible with the RL environment") + with torch.random.fork_rng(devices=[]): + policy = ActorCriticPolicy(obs_dim=int(first.shape[1]), hidden_dim=int(first.shape[0])) + if not all(isinstance(value, torch.Tensor) and torch.is_floating_point(value) and torch.isfinite(value).all() for value in weights.values()): + raise ValueError("checkpoint contains non-finite or invalid weights") + policy.load_state_dict(weights, strict=True) + except Exception as exc: + raise ValueError(f"Invalid RL checkpoint {path.name}: {exc}") from exc + policy.to(device) + policy.eval() + metadata.update({"checkpointFile": path.name, "checkpointSha256": file_sha256(path)}) + return policy, metadata + + +def save_checkpoint(policy: ActorCriticPolicy, path: Path, metadata: dict[str, Any]) -> None: + """Replace checkpoints atomically so interruption cannot destroy a prior save.""" + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(path.name + ".tmp") + torch.save({"state_dict": policy.state_dict(), "metadata": {**metadata, "featureSchema": FEATURE_SCHEMA}}, temporary) + temporary.replace(path) diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/environment.py b/.codex/skills/beat-saber-mapping/scripts/rl/environment.py index db1d13f..4870237 100644 --- a/.codex/skills/beat-saber-mapping/scripts/rl/environment.py +++ b/.codex/skills/beat-saber-mapping/scripts/rl/environment.py @@ -21,6 +21,7 @@ vision_blocking_double, ) from safety_contract import RECOVERY_BEATS, occupied_until +from .safety import ValidationClock, transition_is_safe try: from .rewards import CompositeReward, RewardBreakdown except ImportError: @@ -71,10 +72,14 @@ def __init__( difficulty: str = "Expert", lookahead_steps: int = 4, ): - self.bpm = max(1.0, float(bpm)) + self.bpm = float(bpm) + if not math.isfinite(self.bpm) or self.bpm <= 0: + raise ValueError("RL BPM must be finite and positive") + self.base_bpm = self.bpm self.difficulty = difficulty if difficulty in DIFFICULTIES else "Expert" self.target_nps = TARGET_NPS_MAP.get(self.difficulty, 4.25) self.lookahead_steps = max(1, lookahead_steps) + self.rng = np.random.default_rng(0) self.reward_engine = CompositeReward( difficulty=self.difficulty, @@ -84,9 +89,10 @@ def __init__( # Internal state buffers self.beat_grid = self._normalize_beat_grid(beat_grid) if beat_grid is not None else self._generate_synthetic_grid(64, self.bpm) - self.audio_features = audio_features or self._generate_synthetic_audio(len(self.beat_grid)) + self.audio_features = audio_features if audio_features is not None else self._generate_synthetic_audio(len(self.beat_grid)) self.current_step = 0 self.total_steps = len(self.beat_grid) + self.validation_clock = ValidationClock(self.audio_features, self.beat_grid, self.base_bpm) # Kinematic hand states self.red_prev: Optional[Dict[str, Any]] = None @@ -106,31 +112,25 @@ def _normalize_beat_grid(self, beat_grid: Any) -> List[float]: if beat_grid is None: return self._generate_synthetic_grid(64, self.bpm) if isinstance(beat_grid, dict): - if "beats" in beat_grid and isinstance(beat_grid["beats"], list): - return [float(b) for b in beat_grid["beats"]] - if "tempoRegions" in beat_grid: - max_beat = max((float(r.get("endBeat", 0.0)) for r in beat_grid.get("tempoRegions", [])), default=64.0) - return [round(i * 0.25, 4) for i in range(max(16, int(max_beat * 4)))] - return self._generate_synthetic_grid(64, self.bpm) - return [float(b) for b in beat_grid] + raise ValueError("Use rl.features.load_analysis_features to project an analysis grid") + result = [float(b) for b in beat_grid] + if not result or not np.isfinite(result).all() or np.any(np.diff(result) <= 0): + raise ValueError("Environment beat grid must be nonempty, finite, and strictly increasing") + return result def _generate_synthetic_grid(self, num_beats: int, bpm: float) -> List[float]: # Quarter-beat subdivision return [round(i * 0.25, 4) for i in range(num_beats * 4)] def _generate_synthetic_audio(self, length: int) -> Dict[str, Any]: + stems = ("drums", "bass", "guitar", "piano", "vocals", "other") return { - "onsets": np.random.uniform(0.0, 1.0, size=(length,)).tolist(), - "flux": np.random.uniform(0.0, 1.0, size=(length,)).tolist(), - "stems": { - "drums": np.random.uniform(0.0, 1.0, size=(length,)).tolist(), - "bass": np.random.uniform(0.0, 1.0, size=(length,)).tolist(), - "guitar": np.random.uniform(0.0, 0.5, size=(length,)).tolist(), - "piano": np.random.uniform(0.0, 0.5, size=(length,)).tolist(), - "vocals": np.random.uniform(0.0, 0.8, size=(length,)).tolist(), - "other": np.random.uniform(0.0, 0.5, size=(length,)).tolist(), - }, - "sections": ["verse"] * length, + "onsets": [1.0 if i % 4 == 0 else 0.0 for i in range(length)], + "flux": [0.0] * length, + "stems": {stem: [0.0] * length for stem in stems}, + "stemAvailability": {stem: False for stem in stems}, + "sections": ["unknown"] * length, + "provenance": {"source": "synthetic-test-pulse", "timingStatus": "synthetic"}, } def reset( @@ -142,32 +142,34 @@ def reset( difficulty: Optional[str] = None, ) -> Tuple[np.ndarray, Dict[str, Any]]: if seed is not None: - np.random.seed(seed) + self.rng = np.random.default_rng(seed) if bpm is not None: - self.bpm = max(1.0, float(bpm)) + if not math.isfinite(float(bpm)) or float(bpm) <= 0: + raise ValueError("RL BPM must be finite and positive") + self.bpm = self.base_bpm = float(bpm) if difficulty is not None and difficulty in DIFFICULTIES: self.difficulty = difficulty self.target_nps = TARGET_NPS_MAP.get(self.difficulty, 4.25) - self.reward_engine = CompositeReward( - difficulty=self.difficulty, - bpm=self.bpm, - target_nps=self.target_nps, - ) + self.reward_engine = CompositeReward( + difficulty=self.difficulty, bpm=self.bpm, target_nps=self.target_nps, + ) if beat_grid is not None: self.beat_grid = self._normalize_beat_grid(beat_grid) elif audio_features is not None: - self.beat_grid = self._generate_synthetic_grid(len(audio_features.get("onsets", [])), self.bpm) + self.beat_grid = self._normalize_beat_grid([i * 0.25 for i in range(len(audio_features.get("onsets", [])))]) if audio_features is not None: self.audio_features = audio_features self.current_step = 0 self.total_steps = len(self.beat_grid) + self.validation_clock = ValidationClock(self.audio_features, self.beat_grid, self.base_bpm) self.red_prev = None self.blue_prev = None self.placed_notes = [] + self._update_step_tempo() obs = self._get_observation() info = {"current_step": 0, "beat": self.beat_grid[0] if self.beat_grid else 0.0} @@ -175,10 +177,12 @@ def reset( def action_masks(self) -> Tuple[np.ndarray, np.ndarray]: """Compute binary action masks for Left (Red) and Right (Blue) hands.""" + self._update_step_tempo() mask_red = np.ones(NUM_HAND_ACTIONS, dtype=bool) mask_blue = np.ones(NUM_HAND_ACTIONS, dtype=bool) current_beat = self.beat_grid[self.current_step] if self.current_step < self.total_steps else 0.0 + validation_beat = self.validation_clock.beat(current_beat) recovery_beats = RECOVERY_BEATS.get(self.difficulty, 0.30) # Rhythmic grid subdivision gating @@ -193,7 +197,7 @@ def action_masks(self) -> Tuple[np.ndarray, np.ndarray]: return mask_red, mask_blue # 1. Mask Left Hand (Red, Color 0) - if self.red_prev is not None and (current_beat - float(self.red_prev["b"])) < (recovery_beats - 1e-5): + if self.red_prev is not None and (validation_beat - self.validation_clock.beat(float(self.red_prev["b"]))) < recovery_beats: mask_red[1:] = False else: for act in range(1, NUM_HAND_ACTIONS): @@ -204,14 +208,11 @@ def action_masks(self) -> Tuple[np.ndarray, np.ndarray]: candidate = {"b": current_beat, "c": 0, "x": x, "y": y, "d": d} if self.red_prev is not None: - finding = transition_finding( - self.red_prev, candidate, self.bpm, recovery_beats=recovery_beats - ) - if finding is not None: + if not transition_is_safe(self.validation_clock.note(self.red_prev), self.validation_clock.note(candidate), self.base_bpm, self.difficulty): mask_red[act] = False # 2. Mask Right Hand (Blue, Color 1) - if self.blue_prev is not None and (current_beat - float(self.blue_prev["b"])) < (recovery_beats - 1e-5): + if self.blue_prev is not None and (validation_beat - self.validation_clock.beat(float(self.blue_prev["b"]))) < recovery_beats: mask_blue[1:] = False else: for act in range(1, NUM_HAND_ACTIONS): @@ -222,14 +223,27 @@ def action_masks(self) -> Tuple[np.ndarray, np.ndarray]: candidate = {"b": current_beat, "c": 1, "x": x, "y": y, "d": d} if self.blue_prev is not None: - finding = transition_finding( - self.blue_prev, candidate, self.bpm, recovery_beats=recovery_beats - ) - if finding is not None: + if not transition_is_safe(self.validation_clock.note(self.blue_prev), self.validation_clock.note(candidate), self.base_bpm, self.difficulty): mask_blue[act] = False return mask_red, mask_blue + def blue_action_mask(self, red_action: int, base_mask: Optional[np.ndarray] = None) -> np.ndarray: + """Condition the second hand on the first hand's actual selected action.""" + mask = (base_mask if base_mask is not None else self.action_masks()[1]).copy() + decoded = decode_hand_action(red_action) + if decoded is None or self.current_step >= self.total_steps: + return mask + beat = self.beat_grid[self.current_step] + red = {"b": beat, "c": 0, "x": decoded[0], "y": decoded[1], "d": decoded[2]} + for action in np.flatnonzero(mask[1:]) + 1: + x, y, direction = decode_hand_action(int(action)) + blue = {"b": beat, "c": 1, "x": x, "y": y, "d": direction} + if cross_hand_finding(red, blue) is not None or vision_blocking_double(red, blue): + mask[action] = False + mask[ACTION_IDLE] = True + return mask + def step( self, action: Tuple[int, int] ) -> Tuple[np.ndarray, float, bool, bool, Dict[str, Any]]: @@ -240,6 +254,7 @@ def step( act_red, act_blue = action current_beat = self.beat_grid[self.current_step] + self._update_step_tempo() # Decode actions red_decoded = decode_hand_action(act_red) @@ -269,16 +284,16 @@ def step( # Compute Step Reward breakdown: RewardBreakdown = self.reward_engine.compute_step_reward( - beat=current_beat, - red_prev=self.red_prev, - red_curr=red_curr, - blue_prev=self.blue_prev, - blue_curr=blue_curr, + beat=self.validation_clock.beat(current_beat), + red_prev=self.validation_clock.note(self.red_prev), + red_curr=self.validation_clock.note(red_curr), + blue_prev=self.validation_clock.note(self.blue_prev), + blue_curr=self.validation_clock.note(blue_curr), onset_strength=onset, stem_energy=stem_energy, is_downbeat=is_downbeat, section_type=section_type, - recent_notes=self.placed_notes[-self.history_window:], + recent_notes=[self.validation_clock.note(note) for note in self.placed_notes[-self.history_window:]], current_nps=current_nps, target_nps=self.target_nps, ) @@ -292,6 +307,7 @@ def step( self.blue_prev = blue_curr self.current_step += 1 + self._update_step_tempo() terminated = self.current_step >= self.total_steps truncated = False @@ -310,6 +326,13 @@ def _get_audio_field(self, field: str, idx: int, default: Any) -> Any: return arr[idx] return default + def _update_step_tempo(self) -> None: + if not self.total_steps: + return + value = self._get_audio_field("stepBpms", min(self.current_step, self.total_steps - 1), self.bpm) + self.bpm = float(value) + self.reward_engine.kinematic.bpm = self.base_bpm + def _get_stem_energy(self, stem: str, idx: int) -> float: stems = self.audio_features.get("stems", {}) arr = stems.get(stem) @@ -324,7 +347,11 @@ def _compute_rolling_nps(self) -> float: span_beats = float(recent[-1]["b"]) - float(recent[0]["b"]) if span_beats <= 1e-6: return 0.0 - span_seconds = span_beats * 60.0 / self.bpm + times = self.audio_features.get("timesSeconds") + if times is not None and len(times) == self.total_steps: + span_seconds = float(np.interp(float(recent[-1]["b"]), self.beat_grid, times) - np.interp(float(recent[0]["b"]), self.beat_grid, times)) + else: + span_seconds = span_beats * 60.0 / self.bpm return len(recent) / max(0.1, span_seconds) def _get_observation(self) -> np.ndarray: diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/evaluate.py b/.codex/skills/beat-saber-mapping/scripts/rl/evaluate.py new file mode 100644 index 0000000..e11683f --- /dev/null +++ b/.codex/skills/beat-saber-mapping/scripts/rl/evaluate.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Offline held-out checkpoint evaluation. No downloads, training, or installation.""" + +from __future__ import annotations + +import argparse +from collections import Counter +from datetime import datetime, timezone +import json +import hashlib +from pathlib import Path +from typing import Any + +import numpy as np + +from beatforge_core import ValidationReport +from validate_map import validate_v3 +from timing_export import project_map_timing +from rl.features import AnalysisFeatures, FEATURE_SCHEMA, file_sha256 +from rl.library import load_library + + +def chart_metrics(chart: dict[str, Any], bundle: AnalysisFeatures, difficulty: str) -> dict[str, Any]: + """Report measurable chart properties, without assigning a subjective score.""" + report = ValidationReport() + exported = project_map_timing(chart, bundle.analysis) + structural = validate_v3(exported, f"{difficulty}Standard.dat", report, bpm=bundle.bpm, difficulty=difficulty) + notes = sorted(chart.get("colorNotes", []), key=lambda note: (note["b"], note["c"])) + if not notes: + report.add("error", "EMPTY_CHART", "A playable chart must contain color notes") + duration = bundle.provenance["durationSamples"] / bundle.provenance["sampleRate"] + hand_counts = Counter(int(note["c"]) for note in notes) + transitions = [] + for color in (0, 1): + hand = [note for note in notes if note["c"] == color] + transitions.extend((left["x"], left["y"], left["d"], right["x"], right["y"], right["d"]) + for left, right in zip(hand, hand[1:])) + pattern_counts = Counter(transitions) + labels = bundle.audio_features["sections"] + times = bundle.audio_features["timesSeconds"] + note_onsets = [] + section_counts: Counter[str] = Counter() + for note in notes: + index = int(np.clip(np.searchsorted(bundle.beat_grid, note["b"]), 0, len(labels) - 1)) + section_counts[labels[index]] += 1 + note_onsets.append(bundle.audio_features["onsets"][index]) + section_seconds: Counter[str] = Counter() + for index, label in enumerate(labels): + end = times[index + 1] if index + 1 < len(times) else duration + section_seconds[label] += max(0.0, end - times[index]) + return { + "notes": len(notes), "nps": len(notes) / duration, + "redNotes": hand_counts[0], "blueNotes": hand_counts[1], + "handImbalance": abs(hand_counts[0] - hand_counts[1]) / max(1, len(notes)), + "mostRepeatedTransitionFraction": max(pattern_counts.values(), default=0) / max(1, len(transitions)), + "meanOnsetStrengthAtNotes": float(np.mean(note_onsets)) if note_onsets else 0.0, + "sectionNps": {label: section_counts[label] / seconds for label, seconds in section_seconds.items() if seconds > 0}, + "hardErrors": len(report.errors), "warnings": len(report.warnings), + "errorCodes": dict(Counter(issue.code for issue in report.errors)), + "structural": structural, + "headsetPlaytest": "not_performed", + } + + +def evaluate_checkpoints( + manifest: Path, checkpoints: list[Path], *, split: str = "validation", + difficulty: str = "Expert", seed: int = 0, +) -> dict[str, Any]: + from rl.policy_generator import RLMapGenerator + import torch + + if split not in {"validation", "test"}: + raise ValueError("Evaluation must use a held-out validation or test split") + library = load_library(manifest) + held_out = [(track, bundle) for track, bundle in library if track["split"] == split] + if not held_out: + raise ValueError(f"Manifest has no {split} tracks") + results = [] + for checkpoint in checkpoints: + generator = RLMapGenerator(model_path=Path(checkpoint)) + training = generator.checkpoint_metadata.get("trainingSourceSha256") + if not training: + raise ValueError("Checkpoint has no training audio hashes; held-out independence cannot be verified") + if generator.checkpoint_metadata.get("featureSchema") != FEATURE_SCHEMA: + raise ValueError("Held-out comparison requires the current shared feature schema") + if set(training) & {bundle.provenance["sourceSha256"] for _, bundle in held_out}: + raise ValueError("Held-out split contains audio used to train this checkpoint") + tracks = [] + for track, bundle in held_out: + chart = generator.generate_difficulty(bundle.audio_features, bundle.beat_grid, bundle.bpm, + difficulty=difficulty, deterministic=True, seed=seed) + tracks.append({"id": track["id"], "features": bundle.provenance, + "chartSha256": hashlib.sha256(json.dumps(chart, sort_keys=True).encode()).hexdigest(), + "metrics": chart_metrics(chart, bundle, difficulty)}) + results.append({ + "checkpoint": generator.checkpoint_metadata, "tracks": tracks, + "summary": {"tracks": len(tracks), "hardErrors": sum(t["metrics"]["hardErrors"] for t in tracks), + "meanNps": float(np.mean([t["metrics"]["nps"] for t in tracks])), + "tracksWithoutHardErrors": sum(t["metrics"]["hardErrors"] == 0 for t in tracks)}, + }) + return { + "schemaVersion": 1, "kind": "offline-held-out-checkpoint-evaluation", + "createdAt": datetime.now(timezone.utc).isoformat(), "seed": seed, + "deterministic": True, "split": split, "difficulty": difficulty, + "manifestSha256": file_sha256(manifest), "featureSchema": FEATURE_SCHEMA, + "runtime": {"torch": str(torch.__version__), "numpy": str(np.__version__)}, + "evaluatorSha256": { + name: file_sha256(Path(__file__).parent / name) + for name in ("evaluate.py", "features.py", "environment.py", "policy_generator.py", "models.py", "rewards.py", "safety.py") + } | { + name: file_sha256(Path(__file__).parent.parent / name) + for name in ("validate_map.py", "kinematics.py", "safety_contract.py", "timing_export.py", "beatforge_core.py") + }, + "heldOutVerifiedAgainstRecordedTraining": True, + "limitations": ["Recorded hashes establish data separation; they do not prove prior undocumented training history.", + "Chart validation does not replace a complete packaged-map or headset playtest.", + "Musical preference and manual correction counts require human evaluation."], + "results": results, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--checkpoint", type=Path, action="append", required=True) + parser.add_argument("--split", choices=("validation", "test"), default="validation") + parser.add_argument("--difficulty", choices=("Easy", "Normal", "Hard", "Expert", "ExpertPlus"), default="Expert") + parser.add_argument("--seed", type=int, default=0) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + try: + result = evaluate_checkpoints(args.manifest, args.checkpoint, split=args.split, difficulty=args.difficulty, seed=args.seed) + except (ValueError, FileNotFoundError) as exc: + parser.error(str(exc)) + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(json.dumps(result, indent=2, allow_nan=False) + "\n", encoding="utf-8") + print(f"Saved {len(result['results'])} checkpoint evaluation(s) to {args.out}") + return 2 if any(item["summary"]["hardErrors"] for item in result["results"]) else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/features.py b/.codex/skills/beat-saber-mapping/scripts/rl/features.py new file mode 100644 index 0000000..8b418d7 --- /dev/null +++ b/.codex/skills/beat-saber-mapping/scripts/rl/features.py @@ -0,0 +1,219 @@ +"""One sample-aligned feature contract for RL training, evaluation, and generation. + +Consumes the existing analyzer artifacts. It never estimates or verifies timing. +Missing separated stems are zeros with availability recorded, never mix copies. +""" + +from __future__ import annotations + +from dataclasses import dataclass +import hashlib +import json +import math +from pathlib import Path +from typing import Any + +import numpy as np + +FEATURE_SCHEMA = "beatforge-rl-sample-grid-v2" +STEMS = ("drums", "bass", "guitar", "piano", "vocals", "other") + + +@dataclass +class AnalysisFeatures: + audio_features: dict[str, Any] + beat_grid: list[float] + bpm: float + provenance: dict[str, Any] + analysis: dict[str, Any] + + +def file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _unit(values: np.ndarray) -> np.ndarray: + values = np.maximum(values, 0.0) + scale = max(1.0, float(np.max(values))) if values.size else 1.0 + return np.clip(values / scale, 0.0, 1.0) + + +def build_analysis_features( + analysis: dict[str, Any], + grid_document: dict[str, Any], + sections_document: dict[str, Any], + frames: dict[str, Any], + *, + require_verified: bool = True, +) -> AnalysisFeatures: + """Project frames and onset stem annotations onto actual quarter-beat samples.""" + if not all(isinstance(document, dict) for document in (analysis, grid_document, sections_document, frames)): + raise ValueError("RL analysis artifacts must contain objects") + status = str(analysis.get("status", "unknown")) + if require_verified and status != "timing_verified": + raise ValueError(f"RL requires verified analysis; timing status is {status}") + if grid_document.get("status", status) != status: + raise ValueError("Analysis and beat grid timing statuses disagree") + sample_rate = int(analysis["sampleRate"]) + if float(analysis["sampleRate"]) != sample_rate or float(grid_document.get("sampleRate", sample_rate)) != sample_rate: + raise ValueError("Analysis sample rate must be an integer") + if sample_rate != 44100 or int(grid_document.get("sampleRate", sample_rate)) != sample_rate: + raise ValueError("RL analysis must use the canonical 44100 Hz sample clock") + duration = int(analysis["durationSamples"]) + if float(analysis["durationSamples"]) != duration: + raise ValueError("Analysis durationSamples must be an integer") + bpm = float(analysis["bpm"]) + if duration <= 0 or not math.isfinite(bpm) or bpm <= 0: + raise ValueError("Analysis has invalid duration or BPM") + rows = grid_document.get("beats", []) + if len(rows) < 2 or not all(isinstance(row, dict) for row in rows): + raise ValueError("beat_grid.json must contain at least two beat/sample rows") + beats = np.asarray([row["beat"] for row in rows], dtype=float) + raw_samples = np.asarray([row["sample"] for row in rows], dtype=float) + if (not np.isfinite(beats).all() or not np.isfinite(raw_samples).all() + or np.any(raw_samples != np.round(raw_samples)) + or np.any(np.diff(beats) <= 0) or np.any(np.diff(raw_samples) <= 0)): + raise ValueError("Beat/sample rows must be finite, integer-sampled, and strictly increasing") + if raw_samples[0] < 0 or raw_samples[-1] >= duration: + raise ValueError("Beat samples fall outside the analyzed audio") + if "beatGrid" in analysis: + recorded = [(row["beat"], row["sample"]) for row in analysis["beatGrid"]] + if recorded != [(row["beat"], row["sample"]) for row in rows]: + raise ValueError("Analysis and beat_grid.json contain different adopted timing grids") + + # The analyzer may provide finer subdivisions. Select the quarter-beat domain + # through interpolation in the adopted sample clock, including pickups. + first_tick = math.ceil(beats[0] * 4 - 1e-7) + last_tick = math.floor(beats[-1] * 4 + 1e-7) + steps = np.arange(first_tick, last_tick + 1, dtype=float) / 4.0 + if not len(steps): + raise ValueError("Beat grid contains no quarter-beat decision steps") + samples = np.rint(np.interp(steps, beats, raw_samples)).astype(np.int64) + if np.any(np.diff(samples) <= 0): + raise ValueError("Quarter-beat sample positions must increase") + times = samples / sample_rate + + def frame_values(key: str) -> np.ndarray: + values = np.asarray(frames.get(key, []), dtype=float) + if values.ndim != 1: + raise ValueError(f"Audio feature {key} must be a one-dimensional envelope") + if not np.isfinite(values).all(): + raise ValueError(f"Non-finite audio feature {key}") + return values + + def align(key: str) -> list[float]: + values = frame_values(key) + if not len(values): + return [0.0] * len(steps) + if "samples" in frames: + positions = np.asarray(frames["samples"], dtype=float).reshape(-1) + if np.any(positions != np.round(positions)): + raise ValueError("Audio frame sample positions must be integers") + elif "times" in frames or "timeSeconds" in frames: + positions = np.asarray(frames.get("times", frames.get("timeSeconds")), dtype=float).reshape(-1) * sample_rate + else: + hops = np.asarray(frames.get("hopSamples", frames.get("hop", 0))).reshape(-1) + if len(hops) != 1: + raise ValueError("Audio feature frames require a single hopSamples value") + hop = float(hops[0]) + if not math.isfinite(hop) or hop <= 0 or hop != round(hop): + raise ValueError("Audio feature frames require sample positions, times, or integer hopSamples") + positions = np.arange(len(values)) * hop + if len(positions) != len(values) or not np.isfinite(positions).all() or np.any(np.diff(positions) <= 0): + raise ValueError(f"Invalid frame clock for {key}") + return np.interp(samples, positions, _unit(values), left=0.0, right=0.0).tolist() + + onset_key = next((key for key in ("combinedOnset", "combined", "onsets") if key in frames), None) + if onset_key is None: + raise ValueError("Audio feature artifact is missing its onset envelope") + if not len(frame_values(onset_key)): + raise ValueError("Audio feature artifact contains an empty onset envelope") + onsets = align(onset_key) + flux_available = "flux" in frames and bool(len(frame_values("flux"))) + flux = align("flux") if flux_available else [0.0] * len(steps) + + stems = {stem: [0.0] * len(steps) for stem in STEMS} + available = {stem: False for stem in STEMS} + stem_sources: dict[str, str] = {} + sustains = [0.0] * len(steps) + # Demucs is persisted as event annotations after temporary WAVs are removed. + # Assign an event to its nearest step only; never extend it across silence. + for event in analysis.get("events", []): + energies = event.get("stemEnergy", {}) + sample_value = float(event["sample"]) + if not math.isfinite(sample_value) or sample_value != round(sample_value) or not 0 <= sample_value < duration: + raise ValueError("Analysis onset samples must be integers within the audio") + sample = int(sample_value) + right = min(int(np.searchsorted(samples, sample)), len(samples) - 1) + left = max(0, right - 1) + index = left if abs(int(samples[left]) - sample) <= abs(int(samples[right]) - sample) else right + sustain = float(event.get("sustainBeats", 0.0)) + if not math.isfinite(sustain) or sustain < 0: + raise ValueError("Analysis sustain duration must be finite and nonnegative") + sustains[index] = max(sustains[index], sustain) + for stem in STEMS: + if stem not in energies: + continue + value = float(energies[stem]) + if not math.isfinite(value) or value < 0: + raise ValueError(f"Invalid separated {stem} energy") + available[stem] = True + stem_sources[stem] = "analysis.events.stemEnergy" + stems[stem][index] = max(stems[stem][index], min(value, 1.0)) + + labels = ["unknown"] * len(steps) + for section in sections_document.get("sections", []): + # External segment beat fields may have used a constant BPM conversion. + # Prefer sample/second boundaries to retain offset and tempo changes. + if "startSample" in section and "endSample" in section: + selected = (samples >= int(section["startSample"])) & (samples < int(section["endSample"])) + elif "startSeconds" in section and "endSeconds" in section: + selected = (times >= float(section["startSeconds"])) & (times < float(section["endSeconds"])) + elif "startBeat" in section and "endBeat" in section: + selected = (steps >= float(section["startBeat"])) & (steps < float(section["endBeat"])) + else: + raise ValueError("Section requires a start/end interval") + label = str(section.get("label", section.get("type", "unknown"))) + for index in np.flatnonzero(selected): + labels[int(index)] = label + + intervals = np.searchsorted(beats, steps, side="right") - 1 + intervals = np.clip(intervals, 0, len(beats) - 2) + bpms = 60.0 * sample_rate * np.diff(beats)[intervals] / np.diff(raw_samples)[intervals] + provenance = { + "featureSchema": FEATURE_SCHEMA, "timingStatus": status, + "gridSource": grid_document.get("source", analysis.get("gridSource", "unknown")), + "sourceSha256": analysis.get("sourceSha256"), "sampleRate": sample_rate, + "durationSamples": duration, "decisionSteps": len(steps), + "firstSample": int(samples[0]), "lastSample": int(samples[-1]), + "stemAvailability": available, "stemSources": stem_sources, + "fluxAvailable": flux_available, + "sourceAudioVerified": False, + "sectionSource": sections_document.get("source", "analysis"), + } + audio_features = { + "onsets": onsets, "flux": flux, "stems": stems, "sections": labels, + "sustainBeats": sustains, + "samplePositions": samples.tolist(), "timesSeconds": times.tolist(), + "stepBpms": bpms.tolist(), "stemAvailability": available, + "provenance": provenance, + } + return AnalysisFeatures(audio_features, steps.tolist(), bpm, provenance, {**analysis, "beatGrid": rows}) + + +def load_analysis_features(analysis_dir: Path, *, require_verified: bool = True) -> AnalysisFeatures: + analysis_dir = Path(analysis_dir) + names = ("analysis.json", "beat_grid.json", "sections.json", "audio_features.npz") + for name in names: + if not (analysis_dir / name).is_file(): + raise FileNotFoundError(f"Missing RL analysis artifact: {analysis_dir / name}. Run analyze_audio.py first.") + documents = [json.loads((analysis_dir / name).read_text(encoding="utf-8")) for name in names[:3]] + with np.load(analysis_dir / names[3], allow_pickle=False) as archive: + frames = {key: archive[key] for key in archive.files} + result = build_analysis_features(*documents, frames, require_verified=require_verified) + result.provenance["artifactSha256"] = {name: file_sha256(analysis_dir / name) for name in names} + return result diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/library.py b/.codex/skills/beat-saber-mapping/scripts/rl/library.py new file mode 100644 index 0000000..153b4f1 --- /dev/null +++ b/.codex/skills/beat-saber-mapping/scripts/rl/library.py @@ -0,0 +1,46 @@ +"""Explicit training and held-out song lists, identified by audio content hash.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from .features import AnalysisFeatures, file_sha256, load_analysis_features + + +def load_library(manifest: Path) -> list[tuple[dict[str, Any], AnalysisFeatures]]: + manifest = Path(manifest).resolve() + document = json.loads(manifest.read_text(encoding="utf-8")) + if document.get("schemaVersion") != 1 or not isinstance(document.get("tracks"), list): + raise ValueError("Library manifest requires schemaVersion 1 and a tracks list") + result = [] + identities: set[str] = set() + hashes: set[str] = set() + for track in document["tracks"]: + identity = str(track["id"]) + if identity in identities: + raise ValueError(f"Duplicate track ID: {identity}") + if track.get("split") not in {"train", "validation", "test"}: + raise ValueError(f"Track {identity} requires split train, validation, or test") + path = Path(track["analysisDir"]) + path = path if path.is_absolute() else manifest.parent / path + bundle = load_analysis_features(path) + source = bundle.provenance.get("sourceSha256") + if not isinstance(source, str) or len(source) != 64 or any(c not in "0123456789abcdef" for c in source.lower()): + raise ValueError(f"Track {identity} requires an analyzed audio SHA-256") + source = source.lower() + if track.get("audioPath"): + audio = Path(track["audioPath"]) + audio = audio if audio.is_absolute() else manifest.parent / audio + if file_sha256(audio) != source: + raise ValueError(f"Audio content changed since analysis: {identity}") + bundle.provenance["sourceAudioVerified"] = True + if source in hashes: + raise ValueError(f"Duplicate audio content or split leakage: {identity}") + hashes.add(source) + identities.add(identity) + result.append(({**track, "analysisDir": str(path.resolve())}, bundle)) + if not result: + raise ValueError("Library manifest contains no tracks") + return result diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/models.py b/.codex/skills/beat-saber-mapping/scripts/rl/models.py index 187352b..1b17b8e 100644 --- a/.codex/skills/beat-saber-mapping/scripts/rl/models.py +++ b/.codex/skills/beat-saber-mapping/scripts/rl/models.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Dict, List, Optional, Tuple +from typing import Callable, Dict, List, Optional, Tuple import torch import torch.nn as nn @@ -103,6 +103,7 @@ def get_action_and_value( action: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, mask_red: Optional[torch.Tensor] = None, mask_blue: Optional[torch.Tensor] = None, + blue_mask_fn: Optional[Callable[[torch.Tensor], torch.Tensor]] = None, ) -> Tuple[Tuple[torch.Tensor, torch.Tensor], torch.Tensor, torch.Tensor, torch.Tensor]: """Compute action sampling, log probability, entropy, and state value.""" features = self.encoder(obs) @@ -125,6 +126,9 @@ def get_action_and_value( logits_blue = self.actor_blue(blue_in) # Apply Action Masking for Blue Hand + if blue_mask_fn is not None: + conditional_mask = blue_mask_fn(act_red) + mask_blue = conditional_mask if mask_blue is None else mask_blue & conditional_mask if mask_blue is not None: logits_blue = torch.where(mask_blue, logits_blue, torch.tensor(-1e8, device=logits_blue.device)) @@ -147,6 +151,8 @@ def predict( mask_red: Optional[torch.Tensor] = None, mask_blue: Optional[torch.Tensor] = None, deterministic: bool = False, + blue_mask_fn: Optional[Callable[[torch.Tensor], torch.Tensor]] = None, + generator: Optional[torch.Generator] = None, ) -> Tuple[int, int]: """Inference action selection.""" self.eval() @@ -166,19 +172,20 @@ def predict( if deterministic: act_red = torch.argmax(logits_red, dim=-1) else: - dist_red = Categorical(logits=logits_red) - act_red = dist_red.sample() + act_red = torch.multinomial(torch.softmax(logits_red, dim=-1), 1, generator=generator).squeeze(-1) red_emb = self.red_action_embed(act_red) blue_in = torch.cat([features, red_emb], dim=-1) logits_blue = self.actor_blue(blue_in) + if blue_mask_fn is not None: + conditional_mask = blue_mask_fn(act_red) + mask_blue = conditional_mask if mask_blue is None else mask_blue & conditional_mask if mask_blue is not None: logits_blue = torch.where(mask_blue, logits_blue, torch.tensor(-1e8, device=logits_blue.device)) if deterministic: act_blue = torch.argmax(logits_blue, dim=-1) else: - dist_blue = Categorical(logits=logits_blue) - act_blue = dist_blue.sample() + act_blue = torch.multinomial(torch.softmax(logits_blue, dim=-1), 1, generator=generator).squeeze(-1) return int(act_red.item()), int(act_blue.item()) diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/policy_generator.py b/.codex/skills/beat-saber-mapping/scripts/rl/policy_generator.py index ae83cfe..78e9fff 100644 --- a/.codex/skills/beat-saber-mapping/scripts/rl/policy_generator.py +++ b/.codex/skills/beat-saber-mapping/scripts/rl/policy_generator.py @@ -10,6 +10,9 @@ import numpy as np import torch +from .checkpoints import load_checkpoint +from .safety import safe_hold_candidates +from safety_contract import MIN_CHAIN_DURATION_BEATS try: from .environment import ( @@ -40,11 +43,11 @@ def __init__( self.device = device if policy is not None: self.policy = policy - elif model_path is not None and model_path.exists(): - self.policy = ActorCriticPolicy() - self.policy.load_state_dict(torch.load(model_path, map_location=device)) + self.checkpoint_metadata = {"source": "supplied-policy", "checkpointSha256": None} + elif model_path is not None: + self.policy, self.checkpoint_metadata = load_checkpoint(model_path, device) else: - self.policy = ActorCriticPolicy() + raise ValueError("RL generation requires an explicit trained checkpoint (--rl-model)") self.policy.to(device) self.policy.eval() @@ -56,6 +59,7 @@ def generate_difficulty( bpm: float, difficulty: str = "Expert", deterministic: bool = True, + seed: int = 0, ) -> Dict[str, Any]: """Generate a complete v3.3.0 difficulty map using the RL policy.""" env = BeatSaberEnv( @@ -64,7 +68,8 @@ def generate_difficulty( bpm=bpm, difficulty=difficulty, ) - obs, _ = env.reset() + random = torch.Generator(device=self.device).manual_seed(seed) + obs, _ = env.reset(seed=seed) color_notes: List[Dict[str, Any]] = [] bomb_notes: List[Dict[str, Any]] = [] @@ -80,7 +85,11 @@ def generate_difficulty( t_mask_b = torch.tensor(mask_blue, dtype=torch.bool, device=self.device).unsqueeze(0) act_red, act_blue = self.policy.predict( - t_obs, mask_red=t_mask_r, mask_blue=t_mask_b, deterministic=deterministic + t_obs, mask_red=t_mask_r, mask_blue=t_mask_b, deterministic=deterministic, + blue_mask_fn=lambda selected: torch.as_tensor( + env.blue_action_mask(int(selected.item()), mask_blue), dtype=torch.bool, device=self.device + ).unsqueeze(0), + generator=random, ) current_beat = round(beat_grid[step], 4) @@ -116,10 +125,13 @@ def generate_difficulty( break # Generate Arcs (Sliders) connecting smooth swing trajectories (Post-2022 v3 feature) - sliders = self._generate_arcs(color_notes) + sliders = self._generate_arcs(color_notes, audio_features, beat_grid) # Generate Chains (Burst Sliders) on sustained vibration onsets (Post-2022 v3 feature) burst_sliders = self._generate_chains(color_notes, audio_features, beat_grid) + sliders, burst_sliders, hold_relaxations = safe_hold_candidates( + color_notes, sliders, burst_sliders, audio_features, beat_grid, bpm, difficulty + ) # Generate framing walls & bombs obstacles, bomb_notes = self._generate_obstacles_and_bombs( @@ -150,11 +162,16 @@ def generate_difficulty( "_provenance": { "poseSolver": "rl-policy-v1", "format": "v3.3.0", + "seed": seed, + "checkpoint": self.checkpoint_metadata, + "features": audio_features.get("provenance", {}), + "holdRelaxations": hold_relaxations, + "holdSelection": "audio-evidence-with-fixed-note-safety", }, }, } - def _generate_arcs(self, color_notes: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def _generate_arcs(self, color_notes: List[Dict[str, Any]], audio_features: Dict[str, Any], beat_grid: List[float]) -> List[Dict[str, Any]]: """Generate rich v3 Arcs / Sliders connecting held phrases and melodic transition swings.""" arcs = [] by_color: Dict[int, List[Dict[str, Any]]] = {0: [], 1: []} @@ -166,9 +183,12 @@ def _generate_arcs(self, color_notes: List[Dict[str, Any]]) -> List[Dict[str, An head = notes[i] tail = notes[i + 1] gap = float(tail["b"]) - float(head["b"]) + index = int(np.searchsorted(beat_grid, float(head["b"]))) + sustains = audio_features.get("sustainBeats", []) + sustained = index < len(sustains) and float(sustains[index]) >= gap # Connect notes spaced 0.5 to 3.5 beats apart on melodic transitions - if 0.5 <= gap <= 3.5 and head["d"] != 8 and tail["d"] != 8 and (i % 2 == 0): + if sustained and 0.5 <= gap <= 3.5 and head["d"] != 8 and tail["d"] != 8 and (i % 2 == 0): control = min(2.0, max(0.8, gap * 0.45)) arcs.append({ "b": float(head["b"]), @@ -216,7 +236,7 @@ def _generate_chains( last_chain_beat = -999.0 for i, note in enumerate(color_notes): b = float(note["b"]) - step_idx = min(int(b * 4), len(onsets) - 1) + step_idx = int(np.searchsorted(beat_grid, b)) drum_val = float(drums[step_idx]) if step_idx >= 0 and step_idx < len(drums) and isinstance(drums, list) else 0.0 vocal_val = float(vocals[step_idx]) if step_idx >= 0 and step_idx < len(vocals) and isinstance(vocals, list) else 0.0 @@ -246,8 +266,8 @@ def _generate_chains( ty = min(2, max(0, hy + chosen_dy)) # Determine duration and slice count - dt = 0.0625 if (tx == hx and ty == hy) else (0.125 if abs(chosen_dx) + abs(chosen_dy) <= 1 else 0.25) - sc = 3 if dt <= 0.0625 else (4 if dt <= 0.125 else 5) + dt = MIN_CHAIN_DURATION_BEATS + sc = 5 chains.append({ "b": b, @@ -309,7 +329,7 @@ def notes_in_window(start_b: float, end_b: float) -> List[Dict[str, Any]]: if beat < last_wall_end + min_wall_gap: continue - step_idx = min(int(beat * 4), len(onsets) - 1) + step_idx = i intensity = onsets[step_idx] if 0 <= step_idx < len(onsets) else 0.5 bass_val = bass_energy[step_idx] if 0 <= step_idx < len(bass_energy) else 0.5 diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/registry.py b/.codex/skills/beat-saber-mapping/scripts/rl/registry.py new file mode 100644 index 0000000..2122446 --- /dev/null +++ b/.codex/skills/beat-saber-mapping/scripts/rl/registry.py @@ -0,0 +1,273 @@ +"""Local checkpoint promotion with measured regression gates and rollback. + +Promotion never trains, downloads, installs maps, or asserts human preference. +Stored checkpoints and reports remain immutable by content hash. +""" + +from __future__ import annotations + +import argparse +from contextlib import contextmanager +from datetime import datetime, timezone +import json +import math +import os +from pathlib import Path +import shutil +from typing import Any + +from rl.environment import TARGET_NPS_MAP +from rl.features import FEATURE_SCHEMA, file_sha256 + +PRIMARY_METRIC = "meanOnsetStrengthAtNotes" +MIN_PRIMARY_GAIN = 0.001 + + +def _read(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def _finite(metrics: dict[str, Any], key: str) -> float: + value = metrics.get(key) + if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value): + raise ValueError(f"Evaluation is missing a finite metric: {key}") + return float(value) + + +def _result(report: dict[str, Any], checkpoint_hash: str) -> dict[str, Any]: + if (report.get("kind") != "offline-held-out-checkpoint-evaluation" + or report.get("schemaVersion") != 1 + or report.get("featureSchema") != FEATURE_SCHEMA + or report.get("heldOutVerifiedAgainstRecordedTraining") is not True + or report.get("split") not in {"validation", "test"} + or report.get("deterministic") is not True): + raise ValueError("Promotion requires a verified held-out evaluation with the current schema") + matches = [item for item in report.get("results", []) + if item.get("checkpoint", {}).get("checkpointSha256") == checkpoint_hash] + if len(matches) != 1: + raise ValueError("Evaluation must identify the exact candidate checkpoint once") + selected = matches[0] + training = selected.get("checkpoint", {}).get("trainingSourceSha256") + if not training or not selected.get("tracks"): + raise ValueError("Evaluation must record training hashes and nonempty held-out tracks") + seen: set[str] = set() + for track in selected["tracks"]: + if track["id"] in seen: + raise ValueError("Evaluation repeats a held-out track ID") + seen.add(track["id"]) + features = track.get("features", {}) + source = features.get("sourceSha256") + if not source or source in training or not features.get("artifactSha256"): + raise ValueError("Evaluation contains training overlap or lacks sample artifact identity") + if features.get("timingStatus") != "timing_verified": + raise ValueError("Promotion requires verified timing on every held-out track") + metrics = track["metrics"] + if _finite(metrics, "hardErrors") != 0: + raise ValueError(f"Candidate has hard validation errors on {track['id']}") + if _finite(metrics, "notes") <= 0: + raise ValueError(f"Candidate has an empty chart on {track['id']}") + for key in (PRIMARY_METRIC, "handImbalance", "mostRepeatedTransitionFraction", "nps", "warnings"): + if _finite(metrics, key) < 0: + raise ValueError(f"Metric cannot be negative: {key}") + return selected + + +def compare_evaluations( + baseline: dict[str, Any], baseline_hash: str, + candidate: dict[str, Any], candidate_hash: str, +) -> dict[str, Any]: + """Reject benchmark changes, regressions, and gains below the fixed threshold.""" + before = _result(baseline, baseline_hash) + after = _result(candidate, candidate_hash) + identity = ("manifestSha256", "featureSchema", "split", "difficulty", "seed", "deterministic", "runtime", "evaluatorSha256") + if any(key not in baseline or key not in candidate or baseline[key] != candidate[key] for key in identity): + raise ValueError("Benchmark identity differs; re-evaluate both checkpoints on the same benchmark") + old = {track["id"]: track for track in before["tracks"]} + new = {track["id"]: track for track in after["tracks"]} + if old.keys() != new.keys(): + raise ValueError("Held-out track sets differ") + target = TARGET_NPS_MAP.get(candidate["difficulty"]) + if target is None: + raise ValueError("Unknown evaluation difficulty") + gains = [] + for identity, current in new.items(): + previous = old[identity] + if current["features"] != previous["features"]: + raise ValueError(f"Held-out audio analysis changed: {identity}") + a, b = previous["metrics"], current["metrics"] + for key in ("handImbalance", "mostRepeatedTransitionFraction", "warnings"): + if _finite(b, key) > _finite(a, key) + 1e-9: + raise ValueError(f"Regression in {key} on {identity}") + if abs(_finite(b, "nps") - target) > abs(_finite(a, "nps") - target) + 1e-9: + raise ValueError(f"Regression in target density on {identity}") + gain = _finite(b, PRIMARY_METRIC) - _finite(a, PRIMARY_METRIC) + if gain < -1e-9: + raise ValueError(f"Regression in onset alignment on {identity}") + gains.append(gain) + mean_gain = sum(gains) / len(gains) + if mean_gain < MIN_PRIMARY_GAIN: + raise ValueError(f"Primary metric must improve by at least {MIN_PRIMARY_GAIN}") + return {"primaryMetric": PRIMARY_METRIC, "meanGain": mean_gain, + "minimumGain": MIN_PRIMARY_GAIN, "tracksCompared": len(gains), + "humanPreferenceEstablished": False} + + +@contextmanager +def _locked(root: Path): + root.mkdir(parents=True, exist_ok=True) + lock = root / ".registry.lock" + try: + descriptor = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + except FileExistsError as exc: + raise ValueError("Registry is locked by another operation; retry after it finishes") from exc + try: + os.close(descriptor) + yield + finally: + lock.unlink() + + +def _save_registry(root: Path, state: dict[str, Any]) -> None: + temporary = root / "registry.json.tmp" + temporary.write_text(json.dumps(state, indent=2, allow_nan=False) + "\n", encoding="utf-8") + temporary.replace(root / "registry.json") + + +def _store_artifact(source: Path, destination: Path, expected: str) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + if destination.exists(): + if file_sha256(destination) != expected: + raise ValueError("Stored registry artifact failed its content hash") + return + temporary = destination.with_suffix(destination.suffix + ".tmp") + shutil.copyfile(source, temporary) + if file_sha256(temporary) != expected: + temporary.unlink() + raise ValueError("Artifact changed during registry import") + temporary.replace(destination) + + +def register_checkpoint(root: Path, checkpoint: Path, evaluation: Path, *, initialize: bool = False) -> dict[str, Any]: + """Install a baseline explicitly, or promote a measured improvement atomically.""" + from rl.checkpoints import load_checkpoint + + root, checkpoint, evaluation = Path(root), Path(checkpoint), Path(evaluation) + _, metadata = load_checkpoint(checkpoint) + digest = metadata["checkpointSha256"] + report = _read(evaluation) + report_hash = file_sha256(evaluation) + selected = _result(report, digest) + if selected["checkpoint"].get("trainingSourceSha256") != metadata.get("trainingSourceSha256"): + raise ValueError("Evaluation training lineage does not match checkpoint metadata") + with _locked(root): + state = _read(root / "registry.json") if (root / "registry.json").exists() else {"schemaVersion": 1, "active": None, "models": {}, "history": []} + active = state["active"] + if initialize: + if active is not None: + raise ValueError("Registry already has a baseline; use promote") + decision = {"baselineInitialized": True, "humanPreferenceEstablished": False} + else: + if active is None: + raise ValueError("Initialize an evaluated baseline before promotion") + if active == digest: + raise ValueError("Candidate is already the active checkpoint") + previous = state["models"][active] + baseline_path = root / previous["evaluation"] + if file_sha256(baseline_path) != previous["evaluationSha256"]: + raise ValueError("Stored baseline evaluation changed") + decision = compare_evaluations(_read(baseline_path), active, report, digest) + checkpoint_name = f"checkpoints/{digest}.pt" + evaluation_name = f"evaluations/{report_hash}.json" + _store_artifact(checkpoint, root / checkpoint_name, digest) + _store_artifact(evaluation, root / evaluation_name, report_hash) + state["models"][digest] = {"checkpoint": checkpoint_name, "evaluation": evaluation_name, + "evaluationSha256": report_hash, "metadata": metadata} + state["history"].append({"action": "initialize" if initialize else "promote", "from": active, "to": digest, + "at": datetime.now(timezone.utc).isoformat(), "decision": decision}) + state["active"] = digest + _save_registry(root, state) + return {"active": digest, "previous": active, "checkpoint": str((root / checkpoint_name).resolve()), "decision": decision} + + +def active_checkpoint(root: Path) -> Path: + root = Path(root) + state = _read(root / "registry.json") + digest = state.get("active") + if not digest: + raise ValueError("Registry has no active checkpoint") + if not isinstance(digest, str) or len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest): + raise ValueError("Registry contains an invalid checkpoint identity") + path = root / "checkpoints" / f"{digest}.pt" + if file_sha256(path) != digest: + raise ValueError("Active checkpoint failed its recorded content hash") + return path + + +def registry_status(root: Path) -> dict[str, Any]: + """Compact read-only studio status; does not import PyTorch or expose paths.""" + root = Path(root) + if not (root / "registry.json").is_file(): + return {"status": "empty", "active": None, "modelCount": 0, "automaticTraining": False} + try: + state = _read(root / "registry.json") + active_checkpoint(root) + model = state["models"][state["active"]] + metadata = model.get("metadata", {}) + history = state.get("history", []) + return { + "status": "ready", "active": state["active"], "modelCount": len(state["models"]), + "featureSchema": metadata.get("featureSchema"), + "trainingTrackCount": len(metadata.get("trainingSourceSha256", [])), + "lastAction": history[-1] if history else None, + "automaticTraining": False, "humanPreferenceEstablished": False, + } + except (ValueError, KeyError, TypeError, OSError) as exc: + return {"status": "invalid", "active": None, "automaticTraining": False, + "error": "Registry or stored checkpoint integrity check failed"} + + +def rollback(root: Path) -> dict[str, Any]: + root = Path(root) + with _locked(root): + state = _read(root / "registry.json") + active = state["active"] + previous = next((entry["from"] for entry in reversed(state["history"]) + if entry["to"] == active and entry["from"] is not None), None) + if previous is None: + raise ValueError("No previous active checkpoint exists") + path = root / "checkpoints" / f"{previous}.pt" + if file_sha256(path) != previous: + raise ValueError("Rollback checkpoint failed its recorded content hash") + state["active"] = previous + state["history"].append({"action": "rollback", "from": active, "to": previous, + "at": datetime.now(timezone.utc).isoformat()}) + _save_registry(root, state) + return {"active": previous, "previous": active, "checkpoint": str(path.resolve())} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--registry", required=True, type=Path) + commands = parser.add_subparsers(dest="command", required=True) + for command in ("initialize", "promote"): + sub = commands.add_parser(command) + sub.add_argument("--checkpoint", type=Path, required=True) + sub.add_argument("--evaluation", type=Path, required=True) + commands.add_parser("rollback") + commands.add_parser("active") + args = parser.parse_args() + try: + if args.command == "active": + result = {"checkpoint": str(active_checkpoint(args.registry).resolve())} + elif args.command == "rollback": + result = rollback(args.registry) + else: + result = register_checkpoint(args.registry, args.checkpoint, args.evaluation, initialize=args.command == "initialize") + except (ValueError, FileNotFoundError) as exc: + parser.error(str(exc)) + print(json.dumps(result, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/safety.py b/.codex/skills/beat-saber-mapping/scripts/rl/safety.py new file mode 100644 index 0000000..3cb5d19 --- /dev/null +++ b/.codex/skills/beat-saber-mapping/scripts/rl/safety.py @@ -0,0 +1,86 @@ +"""RL checks in the same constant-BPM coordinates used by the package validator.""" + +from __future__ import annotations + +import math +from collections import Counter +from typing import Any + +import numpy as np + +from kinematics import transition_finding +from safety_contract import MIN_CHAIN_DURATION_BEATS, RECOVERY_BEATS, occupied_until, same_hand_after_hold_too_soon + + +class ValidationClock: + def __init__(self, audio: dict[str, Any], grid: list[float], bpm: float): + self.grid = grid + self.bpm = bpm + times = audio.get("timesSeconds") + if times is None: + self.exported = list(grid) + else: + if len(times) != len(grid) or not np.isfinite(times).all() or np.any(np.diff(times) <= 0): + raise ValueError("RL time positions must match the grid and increase monotonically") + self.exported = [round(float(time) * bpm / 60.0, 8) for time in times] + + def beat(self, beat: float) -> float: + return float(np.interp(beat, self.grid, self.exported)) + + def note(self, note: dict[str, Any] | None) -> dict[str, Any] | None: + return {**note, "b": self.beat(float(note["b"]))} if note is not None else None + + +def transition_is_safe(previous: dict[str, Any], current: dict[str, Any], bpm: float, difficulty: str) -> bool: + if transition_finding(previous, current, bpm, recovery_beats=RECOVERY_BEATS[difficulty]) is not None: + return False + # validate_v3 also enforces this reach cap independently of the swing model. + # Keep validator-based adversarial tests to detect any future contract drift. + gap = float(current["b"]) - float(previous["b"]) + distance = math.hypot(int(current["x"]) - int(previous["x"]), int(current["y"]) - int(previous["y"])) + return not (gap <= .25 and distance > 2.5) + + +def safe_hold_candidates(notes, arcs, chains, audio, grid, bpm, difficulty): + """Keep only decorations compatible with the already selected hand paths. + + The policy currently selects notes, not holds. Decoration must therefore + respect those notes and must never rewrite a valid swing into an unsafe exit. + """ + clock = ValidationClock(audio, grid, bpm) + hands = {color: sorted((clock.note(note) for note in notes if note["c"] == color), key=lambda note: note["b"]) + for color in (0, 1)} + accepted = {"arc": [], "chain": []} + busy = {0: -math.inf, 1: -math.inf} + rejected = Counter() + proposals = [("arc", item) for item in arcs] + [("chain", item) for item in chains] + for kind, item in sorted(proposals, key=lambda proposal: (proposal[1]["b"], proposal[0])): + color = item["c"] + start, tail = clock.beat(item["b"]), clock.beat(item["tb"]) + if item["tb"] > grid[-1] or tail <= start: + rejected["outside_grid"] += 1 + continue + if kind == "chain" and tail - start < MIN_CHAIN_DURATION_BEATS - 1e-8: + rejected["chain_too_short"] += 1 + continue + if start < busy[color]: + rejected["hold_occupancy"] += 1 + continue + hand = hands[color] + inside = [note for note in hand if start + 1e-6 < note["b"] < tail - 1e-6] + if inside or (kind == "chain" and any(abs(note["b"] - tail) <= 1e-6 for note in hand)): + rejected["note_occupancy"] += 1 + continue + next_note = next((note for note in hand if note["b"] > tail + 1e-6), None) + exit_pose = {"b": tail, "c": color, "x": item["tx"], "y": item["ty"], "d": item.get("tc", item["d"])} + if next_note is not None: + gap = next_note["b"] - tail + if same_hand_after_hold_too_soon(gap) or next_note["b"] < occupied_until(tail, RECOVERY_BEATS[difficulty]): + rejected["post_hold_recovery"] += 1 + continue + if not transition_is_safe(exit_pose, next_note, bpm, difficulty): + rejected["tail_exit_motion"] += 1 + continue + accepted[kind].append(item) + busy[color] = occupied_until(tail, RECOVERY_BEATS[difficulty]) + return accepted["arc"], accepted["chain"], dict(rejected) diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/train_custom_tracks.py b/.codex/skills/beat-saber-mapping/scripts/rl/train_custom_tracks.py index ddab251..6a56d98 100644 --- a/.codex/skills/beat-saber-mapping/scripts/rl/train_custom_tracks.py +++ b/.codex/skills/beat-saber-mapping/scripts/rl/train_custom_tracks.py @@ -1,162 +1,122 @@ #!/usr/bin/env python3 -"""Train RL Policy on User Tracks: Daft Punk R.A.M and Ninajirachi.""" +"""Train on verified local analyzer artifacts using the inference feature contract.""" from __future__ import annotations import argparse -import io -import sys from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any -if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", errors="replace") -if hasattr(sys.stderr, "reconfigure"): - sys.stderr.reconfigure(encoding="utf-8", errors="replace") - -import numpy as np import torch -try: - from .environment import BeatSaberEnv - from .models import ActorCriticPolicy - from .train_ppo import train_ppo -except ImportError: - from rl.environment import BeatSaberEnv - from rl.models import ActorCriticPolicy - from rl.train_ppo import train_ppo - -from beatforge_core import AudioBuffer, frame_features, load_audio - - -def extract_track_features(audio_path: Path) -> Tuple[Dict[str, Any], List[float], float]: - """Extract audio features, beat grid, and BPM for RL environment training.""" - print(f"Extracting features from {audio_path.name}...", flush=True) - audio = load_audio(audio_path) - features = frame_features(audio.samples) - hop = int(features["hop"][0]) - sr = audio.sample_rate +from rl.checkpoints import load_checkpoint +from rl.environment import BeatSaberEnv +from rl.features import file_sha256, load_analysis_features +from rl.library import load_library +from rl.models import ActorCriticPolicy +from rl.train_ppo import train_ppo - flux = features["flux"] - bpm_est = 120.0 # default fallback - # Compute rough tempo / beat grid - length = len(flux) - grid_len = max(32, int(audio.duration_samples / (sr * 0.5))) - beat_grid = [round(i * 0.25, 4) for i in range(grid_len)] +def extract_track_features(audio_path: Path, analysis_dir: Path | None = None) -> tuple[dict[str, Any], list[float], float]: + """Load existing analysis; raw audio must match its recorded source hash. - # Interpolate flux into onsets - onsets = np.interp(np.linspace(0, len(flux), grid_len), np.arange(len(flux)), flux) - onsets = (onsets - np.min(onsets)) / (np.max(onsets) - np.min(onsets) + 1e-6) - - # Simulated stem profiles based on transient frequency bands - audio_features = { - "onsets": onsets.tolist(), - "flux": onsets.tolist(), - "stems": { - "drums": (onsets * 0.8 + np.random.uniform(0.0, 0.2, grid_len)).tolist(), - "bass": (onsets * 0.6 + np.random.uniform(0.0, 0.4, grid_len)).tolist(), - "vocals": (onsets * 0.5 + np.random.uniform(0.0, 0.3, grid_len)).tolist(), - "guitar": (onsets * 0.4 + np.random.uniform(0.0, 0.2, grid_len)).tolist(), - "piano": (onsets * 0.3 + np.random.uniform(0.0, 0.2, grid_len)).tolist(), - "other": (onsets * 0.3 + np.random.uniform(0.0, 0.2, grid_len)).tolist(), - }, - "sections": ["verse"] * grid_len, - } - - return audio_features, beat_grid, bpm_est + A directory can be supplied directly. For an audio file, the default artifact + directory is its sibling ``.analysis``. Analysis and human timing review + run before training, so no background model downloads or fake clocks occur. + """ + audio_path = Path(audio_path) + directory = analysis_dir or (audio_path if audio_path.is_dir() else audio_path.with_suffix(".analysis")) + bundle = load_analysis_features(directory) + if audio_path.is_file() and file_sha256(audio_path) != bundle.provenance.get("sourceSha256"): + raise ValueError(f"Analysis does not belong to audio file {audio_path.name}") + if audio_path.is_file(): + bundle.provenance["sourceAudioVerified"] = True + return bundle.audio_features, bundle.beat_grid, bundle.bpm def train_on_custom_library( - tracks: List[Path], + tracks: list[Path], timesteps_per_track: int = 16384, epochs: int = 4, lr: float = 2e-4, model_path: Path = Path("data/models/ppo_policy.pt"), -) -> None: + *, + rounds: int = 1, + seed: int = 0, + resume: bool = True, + held_out_hashes: set[str] | None = None, +) -> dict[str, Any]: + if not tracks or rounds <= 0: + raise ValueError("Training requires tracks and a positive number of rounds") + # Validate the entire library before updating any weights. + prepared = [(Path(track), extract_track_features(Path(track))) for track in tracks] + training_hashes: set[str] = set() + for track, (features, _, _) in prepared: + source = features["provenance"].get("sourceSha256") + if not isinstance(source, str) or len(source) != 64: + raise ValueError(f"Training analysis requires the audio source SHA-256: {track}") + training_hashes.add(source) + held_out_hashes = held_out_hashes or set() + torch.manual_seed(seed) policy = ActorCriticPolicy() - if model_path.is_file(): - try: - policy.load_state_dict(torch.load(model_path, map_location="cpu")) - print(f"Resumed existing weights from {model_path}", flush=True) - except Exception as e: - print(f"Could not load checkpoint: {e}", flush=True) - - print(f"\n=======================================================", flush=True) - print(f" Starting Continuous RL Training on {len(tracks)} User Tracks", flush=True) - print(f"=======================================================\n", flush=True) - - for round_idx in range(1, 100): - print(f"\n--- Training Round {round_idx} Across Library ---", flush=True) - for idx, track_path in enumerate(tracks, 1): - if not track_path.is_file(): - continue - print(f"\n[{idx}/{len(tracks)}] Processing: {track_path.name}", flush=True) - try: - audio_feats, beat_grid, bpm = extract_track_features(track_path) - env = BeatSaberEnv( - audio_features=audio_feats, - beat_grid=beat_grid, - bpm=bpm, - difficulty="Expert", - ) - train_ppo( - policy=policy, - env=env, - total_timesteps=timesteps_per_track, - rollout_steps=512, - ppo_epochs=epochs, - lr=lr, - save_path=model_path, - ) - except Exception as e: - print(f"Error training on {track_path.name}: {e}", flush=True) + prior: dict[str, Any] = {} + if resume and model_path.is_file(): + policy, prior = load_checkpoint(model_path) + if not prior.get("trainingSourceSha256"): + raise ValueError("Checkpoint has no training-library provenance. Use --no-resume for a traceable run.") + training_hashes.update(prior["trainingSourceSha256"]) + if training_hashes & held_out_hashes: + raise ValueError("Held-out audio appears in the checkpoint or requested training library") + + runs = [] + for round_index in range(rounds): + for index, (track, (features, grid, bpm)) in enumerate(prepared): + run_seed = seed + round_index * len(prepared) + index + print(f"Round {round_index + 1}/{rounds}, track {index + 1}/{len(prepared)}: {track.name}", flush=True) + env = BeatSaberEnv(features, grid, bpm, difficulty="Expert") + metrics = train_ppo( + policy, env, total_timesteps=timesteps_per_track, + rollout_steps=min(512, timesteps_per_track), ppo_epochs=epochs, + lr=lr, save_path=model_path, seed=run_seed, + checkpoint_metadata={ + "trainingSourceSha256": sorted(training_hashes), + "heldOutSourceSha256": sorted(held_out_hashes), + "librarySeed": seed, "round": round_index + 1, + "resumedCheckpointSha256": prior.get("checkpointSha256"), + }, + ) + runs.append({"sourceSha256": features["provenance"]["sourceSha256"], **metrics}) + return {"seed": seed, "rounds": rounds, "runs": runs, "checkpoint": str(model_path)} def main() -> int: - parser = argparse.ArgumentParser(description="Train RL policy on specific tracks") - parser.add_argument("--ram-dir", type=Path, default=Path(r"C:\Users\user\Downloads\R.A.M")) - parser.add_argument("--ninajirachi-dir", type=Path, default=Path(r"data\downloads\ninajirachi")) - parser.add_argument("--spotify-dir", type=Path, default=Path(r"data\downloads\spotify_july")) + parser = argparse.ArgumentParser(description=__doc__) + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument("--analysis-dir", action="append", type=Path, help="Repeat for verified analysis directories") + source.add_argument("--manifest", type=Path, help="JSON song library with explicit train/validation/test splits") parser.add_argument("--timesteps-per-track", type=int, default=16384) + parser.add_argument("--rounds", type=int, default=1) + parser.add_argument("--epochs", type=int, default=4) + parser.add_argument("--lr", type=float, default=2e-4) + parser.add_argument("--seed", type=int, default=0) + parser.add_argument("--resume", action=argparse.BooleanOptionalAction, default=True) parser.add_argument("--out", type=Path, default=Path("data/models/ppo_policy.pt")) args = parser.parse_args() - - tracks: List[Path] = [] - if args.ram_dir.is_dir(): - for ext in ("*.mp3", "*.ogg", "*.wav", "*.m4a"): - tracks.extend(sorted(args.ram_dir.glob(ext))) - - downloads_root = Path("data/downloads") - if downloads_root.is_dir(): - for ext in ("**/*.mp3", "**/*.ogg", "**/*.wav", "**/*.webm", "**/*.m4a"): - tracks.extend(sorted(downloads_root.glob(ext))) - - # Deduplicate paths - unique_tracks = [] - seen = set() - for p in tracks: - res = p.resolve() - if res not in seen and p.is_file(): - seen.add(res) - unique_tracks.append(p) - tracks = unique_tracks - - print(f"Found {len(tracks)} training tracks:") - for t in tracks: - print(f" - {t.name}") - - if not tracks: - print("No audio tracks found!") - return 1 - - train_on_custom_library( - tracks=tracks, - timesteps_per_track=args.timesteps_per_track, - model_path=args.out, - ) + try: + held_out: set[str] = set() + tracks = args.analysis_dir or [] + if args.manifest: + library = load_library(args.manifest) + tracks = [Path(track["analysisDir"]) for track, _ in library if track["split"] == "train"] + held_out = {bundle.provenance["sourceSha256"] for track, bundle in library if track["split"] != "train"} + train_on_custom_library( + tracks, args.timesteps_per_track, args.epochs, args.lr, args.out, + rounds=args.rounds, seed=args.seed, resume=args.resume, held_out_hashes=held_out, + ) + except (ValueError, FileNotFoundError) as exc: + parser.error(str(exc)) return 0 if __name__ == "__main__": - sys.exit(main()) + raise SystemExit(main()) diff --git a/.codex/skills/beat-saber-mapping/scripts/rl/train_ppo.py b/.codex/skills/beat-saber-mapping/scripts/rl/train_ppo.py index 480b6f0..d315d4c 100644 --- a/.codex/skills/beat-saber-mapping/scripts/rl/train_ppo.py +++ b/.codex/skills/beat-saber-mapping/scripts/rl/train_ppo.py @@ -12,6 +12,8 @@ import torch import torch.nn as nn import torch.optim as optim +from rl.checkpoints import load_checkpoint, save_checkpoint +from rl.features import load_analysis_features try: from .environment import BeatSaberEnv, NUM_HAND_ACTIONS @@ -70,12 +72,10 @@ def compute_gae(self, last_value: float, gamma: float = 0.99, gae_lambda: float """Compute Generalized Advantage Estimation (GAE).""" last_gae = 0.0 for t in reversed(range(self.ptr)): - if t == self.ptr - 1: - next_val = last_value - next_done = 0.0 - else: - next_val = self.values[t + 1] - next_done = self.dones[t] + next_val = last_value if t == self.ptr - 1 else self.values[t + 1] + # dones[t] belongs to the transition at t, including the last slot. + # After a terminal step the caller's next observation may be a reset. + next_done = self.dones[t] delta = self.rewards[t] + gamma * next_val * (1.0 - next_done) - self.values[t] last_gae = delta + gamma * gae_lambda * (1.0 - next_done) * last_gae @@ -84,7 +84,8 @@ def compute_gae(self, last_value: float, gamma: float = 0.99, gae_lambda: float self.returns[: self.ptr] = self.advantages[: self.ptr] + self.values[: self.ptr] # Normalize advantages valid_adv = self.advantages[: self.ptr] - self.advantages[: self.ptr] = (valid_adv - valid_adv.mean()) / (valid_adv.std() + 1e-8) + if self.ptr: + self.advantages[: self.ptr] = (valid_adv - valid_adv.mean()) / (valid_adv.std(unbiased=False) + 1e-8) def reset(self) -> None: self.ptr = 0 @@ -105,23 +106,30 @@ def train_ppo( vf_coef: float = 0.5, device: str = "cpu", save_path: Optional[Path] = None, + seed: int = 0, + checkpoint_metadata: Optional[Dict[str, Any]] = None, ) -> Dict[str, float]: + if min(total_timesteps, rollout_steps, ppo_epochs, batch_size) <= 0 or lr <= 0: + raise ValueError("Training steps, epochs, batch size, and learning rate must be positive") + torch.manual_seed(seed) + rng = np.random.default_rng(seed) policy.to(device) optimizer = optim.Adam(policy.parameters(), lr=lr, eps=1e-5) buffer = RolloutBuffer(buffer_size=rollout_steps, obs_dim=env.obs_dim, device=device) - obs, _ = env.reset() + obs, _ = env.reset(seed=seed) timesteps_done = 0 iteration = 0 history: Dict[str, List[float]] = {"ep_return": [], "policy_loss": [], "value_loss": []} + current_ep_reward = 0.0 + completed_returns: List[float] = [] while timesteps_done < total_timesteps: buffer.reset() ep_rewards = [] - current_ep_reward = 0.0 policy.eval() - for step in range(rollout_steps): + for step in range(min(rollout_steps, total_timesteps - timesteps_done)): mask_red, mask_blue = env.action_masks() t_obs = torch.tensor(obs, dtype=torch.float32, device=device).unsqueeze(0) t_mask_r = torch.tensor(mask_red, dtype=torch.bool, device=device).unsqueeze(0) @@ -129,15 +137,28 @@ def train_ppo( with torch.no_grad(): (act_red, act_blue), log_prob, _, val = policy.get_action_and_value( - t_obs, mask_red=t_mask_r, mask_blue=t_mask_b + t_obs, mask_red=t_mask_r, mask_blue=t_mask_b, + blue_mask_fn=lambda selected: torch.as_tensor( + env.blue_action_mask(int(selected.item()), mask_blue), dtype=torch.bool, device=device + ).unsqueeze(0), ) a_r = int(act_red.item()) a_b = int(act_blue.item()) + # Replay must use the same conditional distribution as collection. + mask_blue = env.blue_action_mask(a_r, mask_blue) next_obs, reward, terminated, truncated, info = env.step((a_r, a_b)) current_ep_reward += reward + # A time limit is not an MDP terminal. Bootstrap its final observation, + # then cut the advantage trace before the reset episode. + if truncated and not terminated: + with torch.no_grad(): + final_obs = torch.as_tensor(next_obs, dtype=torch.float32, device=device).unsqueeze(0) + final_value = policy.forward(final_obs)[2].item() + reward += gamma * float(final_value) + buffer.add( obs=obs, act_red=a_r, @@ -155,12 +176,13 @@ def train_ppo( if terminated or truncated: ep_rewards.append(current_ep_reward) + completed_returns.append(current_ep_reward) current_ep_reward = 0.0 obs, _ = env.reset() with torch.no_grad(): t_obs = torch.tensor(obs, dtype=torch.float32, device=device).unsqueeze(0) - _, _, _, last_val = policy.get_action_and_value(t_obs) + _, _, last_val = policy.forward(t_obs) last_value = float(last_val.item()) buffer.compute_gae(last_value=last_value, gamma=gamma, gae_lambda=gae_lambda) @@ -173,7 +195,7 @@ def train_ppo( indices = np.arange(buffer.ptr) for epoch in range(ppo_epochs): - np.random.shuffle(indices) + rng.shuffle(indices) for start in range(0, len(indices), batch_size): end = start + batch_size mb_idx = indices[start:end] @@ -229,11 +251,21 @@ def train_ppo( if save_path is not None: save_path.parent.mkdir(parents=True, exist_ok=True) - torch.save(policy.state_dict(), save_path) + metadata = dict(checkpoint_metadata or {}) + source = env.audio_features.get("provenance", {}).get("sourceSha256") + if source: + metadata["trainingSourceSha256"] = sorted(set(metadata.get("trainingSourceSha256", [])) | {source}) + save_checkpoint(policy, save_path, { + **metadata, "algorithm": "ppo", "seed": seed, + "lastRunTimesteps": timesteps_done, + "features": env.audio_features.get("provenance", {}), + }) print(f"Saved trained PPO policy to {save_path}", flush=True) return { - "final_mean_reward": float(np.mean(ep_rewards) if ep_rewards else current_ep_reward), + "final_mean_reward": float(np.mean(completed_returns) if completed_returns else current_ep_reward), + "timesteps": timesteps_done, + "completed_episodes": len(completed_returns), "policy_loss": avg_p_loss, "value_loss": avg_v_loss, } @@ -244,18 +276,28 @@ def main() -> int: parser.add_argument("--timesteps", type=int, default=1024) parser.add_argument("--rollout-steps", type=int, default=256) parser.add_argument("--epochs", type=int, default=4) - parser.add_argument("--resume", action="store_true", default=True, help="Resume from existing checkpoint if available") + parser.add_argument("--resume", action=argparse.BooleanOptionalAction, default=True) + parser.add_argument("--lr", type=float, default=3e-4) + parser.add_argument("--seed", type=int, default=0) + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument("--analysis-dir", type=Path, help="Verified analyzer artifact directory") + source.add_argument("--synthetic", action="store_true", help="Explicit smoke-test pulse, not real-song training") parser.add_argument("--out", type=Path, default=Path("data/models/ppo_policy.pt")) args = parser.parse_args() - env = BeatSaberEnv(difficulty="Expert") + torch.manual_seed(args.seed) + if args.analysis_dir: + bundle = load_analysis_features(args.analysis_dir) + env = BeatSaberEnv(bundle.audio_features, bundle.beat_grid, bundle.bpm, difficulty="Expert") + else: + env = BeatSaberEnv(difficulty="Expert") policy = ActorCriticPolicy() + prior_metadata = {} if args.resume and args.out.is_file(): - try: - policy.load_state_dict(torch.load(args.out, map_location="cpu")) - print(f"Resumed policy weights from {args.out}", flush=True) - except Exception as e: - print(f"Could not load checkpoint: {e}, starting from scratch", flush=True) + policy, prior_metadata = load_checkpoint(args.out) + if args.analysis_dir and not prior_metadata.get("trainingSourceSha256"): + parser.error("Checkpoint training lineage is unknown. Use --no-resume to start a traceable run.") + print(f"Resumed policy weights from {args.out}", flush=True) train_ppo( policy=policy, @@ -265,6 +307,8 @@ def main() -> int: ppo_epochs=args.epochs, lr=args.lr, save_path=args.out, + seed=args.seed, + checkpoint_metadata=prior_metadata, ) return 0 diff --git a/.codex/skills/beat-saber-mapping/scripts/timing_export.py b/.codex/skills/beat-saber-mapping/scripts/timing_export.py new file mode 100644 index 0000000..c0c180d --- /dev/null +++ b/.codex/skills/beat-saber-mapping/scripts/timing_export.py @@ -0,0 +1,83 @@ +"""Project authored musical beats into the constant BPM clock used by exported maps.""" + +from __future__ import annotations + +from bisect import bisect_right +from copy import deepcopy +import math +from typing import Any + + +class ExportClock: + def __init__(self, analysis: dict[str, Any]): + self.bpm = float(analysis["bpm"]) + self.sample_rate = int(analysis.get("sampleRate", 44100)) + rows = analysis.get("beatGrid", []) + if len(rows) < 2: + raise ValueError("Export timing requires at least two adopted beat/sample rows") + self.beats = [float(row["beat"]) for row in rows] + self.samples = [float(row["sample"]) for row in rows] + if not math.isfinite(self.bpm) or self.bpm <= 0 or self.sample_rate <= 0: + raise ValueError("Invalid export BPM or sample rate") + if not all(math.isfinite(value) for value in self.beats + self.samples): + raise ValueError("Export timing grid contains a non-finite position") + if any(right <= left for values in (self.beats, self.samples) for left, right in zip(values, values[1:])): + raise ValueError("Export timing grid must increase in beat and sample positions") + + def beat(self, authored: float) -> float: + index = min(len(self.beats) - 2, max(0, bisect_right(self.beats, authored) - 1)) + ratio = (authored - self.beats[index]) / (self.beats[index + 1] - self.beats[index]) + sample = self.samples[index] + ratio * (self.samples[index + 1] - self.samples[index]) + # Round to the source sample clock before writing fractional export beats. + return round(round(sample) / self.sample_rate * self.bpm / 60.0, 8) + + +def project_map_timing(beatmap: dict[str, Any], analysis: dict[str, Any]) -> dict[str, Any]: + """Keep absolute and relative generated v3 event times on the analyzed sample clock.""" + clock = ExportClock(analysis) + result = deepcopy(beatmap) + if result.get("customData", {}).get("_beatforgeTiming"): + raise ValueError("Map timing has already been projected") + if result.get("bpmEvents"): + raise ValueError("Authored maps with BPM events require an explicit BPM-event conversion") + for key in ("colorNotes", "bombNotes", "sliders", "burstSliders", "obstacles", "rotationEvents", "basicBeatmapEvents", "colorBoostBeatmapEvents"): + for item in result.get(key, []): + start = float(item["b"]) + item["b"] = clock.beat(start) + if key in {"sliders", "burstSliders"}: + item["tb"] = clock.beat(float(item["tb"])) + elif key == "obstacles": + item["d"] = round(clock.beat(start + float(item["d"])) - item["b"], 8) + if key in {"basicBeatmapEvents", "colorBoostBeatmapEvents"}: + item["b"] = max(0.0, item["b"]) + for key in ("lightColorEventBoxGroups", "lightRotationEventBoxGroups", "lightTranslationEventBoxGroups"): + for group in result.get(key, []): + start = float(group["b"]) + exported_start = clock.beat(start) + group["b"] = max(0.0, exported_start) + for box in group.get("e", []): + # Only event arrays contain relative beat times. The filter's b is a flag. + for array in ("e", "l", "t"): + for event in box.get(array, []): + if "b" in event: + event["b"] = round(clock.beat(start + float(event["b"])) - exported_start, 8) + result["bpmEvents"] = [] + result.setdefault("customData", {})["_beatforgeTiming"] = { + "schemaVersion": 1, "clock": "constant-bpm-sample-projection", + "sampleRate": clock.sample_rate, "bpm": clock.bpm, + } + return result + + +def project_plan_timing(plan: dict[str, Any], analysis: dict[str, Any]) -> dict[str, Any]: + clock = ExportClock(analysis) + result = deepcopy(plan) + for section in result.get("sections", []): + for key in ("startBeat", "endBeat"): + if key in section: + section["authored" + key[0].upper() + key[1:]] = section[key] + section[key] = clock.beat(float(section[key])) + section["startSeconds"] = float(section["startBeat"]) * 60.0 / clock.bpm + section["endSeconds"] = float(section["endBeat"]) * 60.0 / clock.bpm + result["exportClock"] = "constant-bpm-sample-projection" + return result diff --git a/.codex/skills/beat-saber-mapping/tests/test_rl_environment.py b/.codex/skills/beat-saber-mapping/tests/test_rl_environment.py index 91d5d32..4822f82 100644 --- a/.codex/skills/beat-saber-mapping/tests/test_rl_environment.py +++ b/.codex/skills/beat-saber-mapping/tests/test_rl_environment.py @@ -179,7 +179,7 @@ def test_ppo_training_loop(): def test_rl_map_generator_v3(): - generator = RLMapGenerator() + generator = RLMapGenerator(policy=ActorCriticPolicy()) bpm = 128.0 grid = [round(i * 0.25, 4) for i in range(32 * 4)] # 32 beats audio = { diff --git a/.codex/skills/beat-saber-mapping/tests/test_rl_features.py b/.codex/skills/beat-saber-mapping/tests/test_rl_features.py new file mode 100644 index 0000000..b795c91 --- /dev/null +++ b/.codex/skills/beat-saber-mapping/tests/test_rl_features.py @@ -0,0 +1,115 @@ +"""Regression checks for real analysis clocks and truthful RL inputs.""" + +import json + +import numpy as np +import pytest + +from rl.features import build_analysis_features, load_analysis_features, STEMS +from rl.library import load_library + + +def documents(): + # Offset is half a second; tempo changes from 120 to 60 at beat 1. + rows = [{"beat": beat, "sample": sample} for beat, sample in + [(0, 22050), (.5, 33075), (1, 44100), (1.5, 66150), (2, 88200)]] + analysis = {"status": "timing_verified", "sampleRate": 44100, "durationSamples": 110250, + "bpm": 120, "sourceSha256": "a" * 64, + "events": [{"sample": 44100, "stemEnergy": {"drums": .8, "bass": .2}}]} + grid = {"sampleRate": 44100, "status": "timing_verified", "beats": rows, "source": "confirmed"} + sections = {"source": "all-in-one", "sections": [ + {"label": "verse", "startSeconds": 0, "endSeconds": 1, "startBeat": 0, "endBeat": 99}, + {"label": "chorus", "startSeconds": 1, "endSeconds": 2.5, "startBeat": 99, "endBeat": 100}, + ]} + frames = {"times": np.arange(11) * .25, "combinedOnset": np.arange(11) / 10, + "flux": np.linspace(1, 0, 11)} + return analysis, grid, sections, frames + + +def write_analysis(path, *, source="a" * 64): + path.mkdir(parents=True, exist_ok=True) + analysis, grid, sections, frames = documents() + analysis["sourceSha256"] = source + for name, doc in [("analysis.json", analysis), ("beat_grid.json", grid), ("sections.json", sections)]: + (path / name).write_text(json.dumps(doc), encoding="utf-8") + np.savez_compressed(path / "audio_features.npz", **frames) + return path + + +def test_offset_and_tempo_changes_use_actual_sample_positions(): + result = build_analysis_features(*documents()) + assert result.beat_grid == [i / 4 for i in range(9)] + assert result.audio_features["samplePositions"] == [22050, 27562, 33075, 38588, 44100, 55125, 66150, 77175, 88200] + assert result.audio_features["onsets"][0] == pytest.approx(.2) + assert result.audio_features["onsets"][-1] == pytest.approx(.8) + assert result.audio_features["stepBpms"][:4] == pytest.approx([120] * 4) + assert result.audio_features["stepBpms"][4:] == pytest.approx([60] * 5) + assert result.provenance["durationSamples"] == 110250 + + +def test_sections_use_seconds_over_stale_constant_tempo_beat_fields(): + result = build_analysis_features(*documents()) + assert result.audio_features["sections"] == ["verse"] * 4 + ["chorus"] * 5 + + +def test_stems_are_real_localized_annotations_and_missing_stems_are_honest(): + result = build_analysis_features(*documents()) + audio = result.audio_features + assert audio["stems"]["drums"] == [0, 0, 0, 0, .8, 0, 0, 0, 0] + assert audio["stemAvailability"]["drums"] is True + assert audio["stemAvailability"]["vocals"] is False + assert audio["stems"]["vocals"] == [0] * 9 + analysis, grid, sections, frames = documents() + analysis["events"] = [] + missing = build_analysis_features(analysis, grid, sections, frames) + assert all(not missing.audio_features["stemAvailability"][stem] for stem in STEMS) + assert all(not any(values) for values in missing.audio_features["stems"].values()) + + +def test_hop_clock_does_not_stretch_frames_to_grid_length(): + analysis, grid, sections, _ = documents() + frames = {"hopSamples": 11025, "combinedOnset": np.arange(11) / 10} + result = build_analysis_features(analysis, grid, sections, frames) + assert result.audio_features["onsets"][0] == pytest.approx(.2) + assert result.audio_features["onsets"][-1] == pytest.approx(.8) + assert result.audio_features["flux"] == [0] * 9 + assert result.provenance["fluxAvailable"] is False + + +@pytest.mark.parametrize("bad", [float("nan"), 33074.5, 22050]) +def test_invalid_sample_grid_fails(bad): + args = list(documents()) + args[1]["beats"][1]["sample"] = bad + with pytest.raises(ValueError, match="strictly increasing"): + build_analysis_features(*args) + + +def test_unconfirmed_timing_requires_explicit_generation_override(): + analysis, grid, sections, frames = documents() + analysis["status"] = grid["status"] = "needs_anchors" + with pytest.raises(ValueError, match="verified"): + build_analysis_features(analysis, grid, sections, frames) + allowed = build_analysis_features(analysis, grid, sections, frames, require_verified=False) + assert allowed.provenance["timingStatus"] == "needs_anchors" + + +def test_artifact_hashes_and_repeatability(tmp_path): + path = write_analysis(tmp_path / "analysis") + first = load_analysis_features(path) + assert first == load_analysis_features(path) + assert len(first.provenance["artifactSha256"]) == 4 + (path / "audio_features.npz").unlink() + with pytest.raises(FileNotFoundError, match="Run analyze_audio"): + load_analysis_features(path) + + +def test_library_rejects_content_overlap_even_if_track_names_differ(tmp_path): + write_analysis(tmp_path / "one") + write_analysis(tmp_path / "two") + manifest = tmp_path / "library.json" + manifest.write_text(json.dumps({"schemaVersion": 1, "tracks": [ + {"id": "first", "analysisDir": "one", "split": "train"}, + {"id": "different-name", "analysisDir": "two", "split": "validation"}, + ]})) + with pytest.raises(ValueError, match="split leakage"): + load_library(manifest) diff --git a/.codex/skills/beat-saber-mapping/tests/test_rl_invariants.py b/.codex/skills/beat-saber-mapping/tests/test_rl_invariants.py new file mode 100644 index 0000000..2ef21c1 --- /dev/null +++ b/.codex/skills/beat-saber-mapping/tests/test_rl_invariants.py @@ -0,0 +1,113 @@ +"""Independent serialization, episode-boundary, and reproducibility regressions.""" + +from collections import Counter + +import numpy as np +import pytest + +torch = pytest.importorskip("torch") + +from beatforge_core import ValidationReport +from rl.checkpoints import save_checkpoint +from rl.environment import BeatSaberEnv +from rl.models import ActorCriticPolicy +from rl.policy_generator import RLMapGenerator +from rl.safety import safe_hold_candidates +from rl.train_ppo import RolloutBuffer, train_ppo +from timing_export import project_map_timing +from validate_map import validate_v3 + + +def fixture_audio(length=96, bpm=160, variable=False): + grid = [index / 4 for index in range(length)] + tempos = [90 if index < length // 2 else 220 for index in range(length)] if variable else [bpm] * length + samples = [11025] + for index in range(length - 1): + samples.append(samples[-1] + round(44100 * 60 / tempos[index] / 4)) + audio = { + "onsets": [.95] * length, "flux": [.8] * length, "sustainBeats": [2.] * length, + "sections": ["chorus"] * length, + "stems": {name: [.9] * length for name in ("drums", "bass", "guitar", "piano", "vocals", "other")}, + "timesSeconds": [sample / 44100 for sample in samples], "stepBpms": tempos, + } + analysis = {"bpm": bpm, "sampleRate": 44100, + "beatGrid": [{"beat": beat, "sample": sample} for beat, sample in zip(grid, samples)]} + return audio, grid, analysis + + +@pytest.mark.parametrize("difficulty", ["Easy", "Hard", "ExpertPlus"]) +@pytest.mark.parametrize("bpm,variable", [(90, False), (220, False), (120, True)]) +@pytest.mark.parametrize("seed", [0, 3]) +def test_dense_adversarial_rl_output_passes_independent_exported_validator(difficulty, bpm, variable, seed): + torch.manual_seed(seed) + audio, grid, analysis = fixture_audio(bpm=bpm, variable=variable) + chart = RLMapGenerator(policy=ActorCriticPolicy(hidden_dim=16)).generate_difficulty( + audio, grid, bpm, difficulty=difficulty, deterministic=False, seed=seed, + ) + report = ValidationReport() + validate_v3(project_map_timing(chart, analysis), "fixture.dat", report, bpm=bpm, difficulty=difficulty) + assert len(chart["colorNotes"]) > len(grid) // 8, "Passing by emitting an empty or nearly empty chart is not acceptable" + assert not report.errors, dict(Counter(issue.code for issue in report.errors)) + + +@pytest.mark.parametrize("deterministic", [True, False]) +def test_checkpoint_generation_is_reproducible_and_preserves_caller_rng(tmp_path, deterministic): + torch.manual_seed(17) + checkpoint = tmp_path / "policy.pt" + save_checkpoint(ActorCriticPolicy(hidden_dim=16), checkpoint, {"trainingSourceSha256": ["a" * 64]}) + audio, grid, _ = fixture_audio(length=40) + state = torch.random.get_rng_state().clone() + first = RLMapGenerator(model_path=checkpoint).generate_difficulty(audio, grid, 160, deterministic=deterministic, seed=42) + assert torch.equal(state, torch.random.get_rng_state()) + torch.rand(13) # Unrelated application use must not change the map's random stream. + second = RLMapGenerator(model_path=checkpoint).generate_difficulty(audio, grid, 160, deterministic=deterministic, seed=42) + assert first == second + if not deterministic: + other = RLMapGenerator(model_path=checkpoint).generate_difficulty(audio, grid, 160, deterministic=False, seed=43) + assert other["colorNotes"] != first["colorNotes"] + + +def test_safe_holds_survive_without_erasing_notes_or_overlapping_ownership(): + notes = [{"b": beat, "c": 0, "x": 1, "y": 0, "d": direction} for beat, direction in [(0, 1), (1, 0), (2, 1), (4, 0)]] + arc = {"b": 0, "c": 0, "x": 1, "y": 0, "d": 1, "tb": 1, "tx": 1, "ty": 0, "tc": 0, "mu": 1, "tmu": 1, "m": 0} + chain = {"b": 2, "c": 0, "x": 1, "y": 0, "d": 1, "tb": 2.25, "tx": 1, "ty": 0, "sc": 5, "s": 1} + conflicting = {**chain, "b": 0, "tb": .25} + grid = [index / 4 for index in range(17)] + arcs, chains, rejected = safe_hold_candidates(notes, [arc], [chain, conflicting], {}, grid, 120, "Easy") + assert arcs == [arc] and chains == [chain] + assert rejected["hold_occupancy"] == 1 + report = ValidationReport() + validate_v3({"version": "3.3.0", "colorNotes": notes, "sliders": arcs, "burstSliders": chains}, + "fixture.dat", report, bpm=120, difficulty="Easy") + assert not report.errors + assert len(notes) == 4 + + +@pytest.mark.parametrize("terminal,expected", [(True, 2.), (False, 8.3)]) +def test_actual_ppo_rollout_distinguishes_terminal_and_time_limit(monkeypatch, terminal, expected): + class BoundaryEnv(BeatSaberEnv): + def reset(self, **kwargs): + super().reset(**kwargs) + return np.zeros(self.obs_dim, dtype=np.float32), {} + + def step(self, action): + return np.ones(self.obs_dim, dtype=np.float32), 2., terminal, not terminal, {} + + policy = ActorCriticPolicy(hidden_dim=16) + # Final observation has value 7; reset observation has deliberately huge value + # 123. Neither a true terminal nor the new episode may bootstrap from 123. + monkeypatch.setattr(policy, "forward", lambda obs: (None, None, torch.where(obs[:, 0] > 0, 7., 123.))) + recorded = {} + original = RolloutBuffer.compute_gae + + def inspect(self, *args, **kwargs): + original(self, *args, **kwargs) + recorded["return"] = self.returns[0].item() + recorded["reward"] = self.rewards[0].item() + recorded["done"] = self.dones[0].item() + + monkeypatch.setattr(RolloutBuffer, "compute_gae", inspect) + result = train_ppo(policy, BoundaryEnv(beat_grid=[0, .25]), total_timesteps=1, + rollout_steps=1, ppo_epochs=1, batch_size=1, gamma=.9, seed=4) + assert recorded == pytest.approx({"return": expected, "reward": expected, "done": 1.}) + assert result["final_mean_reward"] == 2., "Reported episode reward must exclude the bootstrap value" diff --git a/.codex/skills/beat-saber-mapping/tests/test_rl_registry.py b/.codex/skills/beat-saber-mapping/tests/test_rl_registry.py new file mode 100644 index 0000000..6171bae --- /dev/null +++ b/.codex/skills/beat-saber-mapping/tests/test_rl_registry.py @@ -0,0 +1,122 @@ +"""Promotion requires the same benchmark, measured progress, and no regressions.""" + +import copy +import json + +import pytest + +from rl.features import FEATURE_SCHEMA +from rl.registry import active_checkpoint, compare_evaluations, register_checkpoint, rollback, registry_status + + +def evaluation(checkpoint_hash, onset=.5): + return { + "schemaVersion": 1, "kind": "offline-held-out-checkpoint-evaluation", + "featureSchema": FEATURE_SCHEMA, "heldOutVerifiedAgainstRecordedTraining": True, + "split": "validation", "difficulty": "Expert", "seed": 0, "deterministic": True, + "manifestSha256": "d" * 64, "runtime": {"torch": "test", "numpy": "test"}, + "evaluatorSha256": {"evaluate.py": "e" * 64}, + "results": [{"checkpoint": {"checkpointSha256": checkpoint_hash, "trainingSourceSha256": ["a" * 64]}, + "tracks": [{"id": "held-out", "features": { + "sourceSha256": "b" * 64, "timingStatus": "timing_verified", + "artifactSha256": {"beat_grid.json": "c" * 64}, + }, "metrics": {"hardErrors": 0, "notes": 100, "nps": 4.25, + "handImbalance": .1, "mostRepeatedTransitionFraction": .1, + "warnings": 0, "meanOnsetStrengthAtNotes": onset}}]}], + } + + +def test_promotion_requires_primary_gain_and_preserves_other_metrics(): + result = compare_evaluations(evaluation("before"), "before", evaluation("after", .6), "after") + assert result["meanGain"] == pytest.approx(.1) + assert result["humanPreferenceEstablished"] is False + + +@pytest.mark.parametrize("key,value", [ + ("hardErrors", 1), ("notes", 0), ("handImbalance", .2), + ("mostRepeatedTransitionFraction", .3), ("warnings", 1), ("nps", 7), + ("meanOnsetStrengthAtNotes", .5), ("meanOnsetStrengthAtNotes", float("nan")), +]) +def test_regression_or_no_improvement_cannot_promote(key, value): + candidate = evaluation("after", .7) + candidate["results"][0]["tracks"][0]["metrics"][key] = value + with pytest.raises(ValueError): + compare_evaluations(evaluation("before"), "before", candidate, "after") + + +@pytest.mark.parametrize("field,value", [("manifestSha256", "different"), ("seed", 4), + ("difficulty", "Hard"), ("evaluatorSha256", {})]) +def test_changed_benchmark_cannot_promote(field, value): + candidate = evaluation("after", .7) + candidate[field] = value + with pytest.raises(ValueError, match="Benchmark identity"): + compare_evaluations(evaluation("before"), "before", candidate, "after") + + +def test_one_improved_track_cannot_hide_a_regression_elsewhere(): + baseline = evaluation("before") + second = copy.deepcopy(baseline["results"][0]["tracks"][0]) + second["id"] = "second" + second["features"]["sourceSha256"] = "f" * 64 + baseline["results"][0]["tracks"].append(second) + candidate = copy.deepcopy(baseline) + candidate["results"][0]["checkpoint"]["checkpointSha256"] = "after" + candidate["results"][0]["tracks"][0]["metrics"]["meanOnsetStrengthAtNotes"] = 1 + candidate["results"][0]["tracks"][1]["metrics"]["meanOnsetStrengthAtNotes"] = .4 + with pytest.raises(ValueError, match="Regression in onset alignment"): + compare_evaluations(baseline, "before", candidate, "after") + + +def test_registry_promotion_and_rollback_preserve_checkpoint_files(tmp_path): + torch = pytest.importorskip("torch") + from rl.checkpoints import load_checkpoint, save_checkpoint + from rl.models import ActorCriticPolicy + paths = [] + for seed in (1, 2, 3): + torch.manual_seed(seed) + path = tmp_path / f"policy-{seed}.pt" + save_checkpoint(ActorCriticPolicy(hidden_dim=16), path, {"trainingSourceSha256": ["a" * 64]}) + _, metadata = load_checkpoint(path) + report_path = tmp_path / f"report-{seed}.json" + report_path.write_text(json.dumps(evaluation(metadata["checkpointSha256"], .4 + seed / 10))) + paths.append((path, report_path)) + registry = tmp_path / "registry" + baseline = register_checkpoint(registry, *paths[0], initialize=True) + previous_bytes = active_checkpoint(registry).read_bytes() + promoted = register_checkpoint(registry, *paths[1]) + assert promoted["previous"] == baseline["active"] + assert active_checkpoint(registry).read_bytes() == paths[1][0].read_bytes() + status = registry_status(registry) + assert status["status"] == "ready" + assert status["modelCount"] == 2 + assert status["automaticTraining"] is False + assert (registry / "checkpoints" / f"{baseline['active']}.pt").read_bytes() == previous_bytes + reverted = rollback(registry) + assert reverted["active"] == baseline["active"] + assert active_checkpoint(registry).read_bytes() == previous_bytes + assert len(list((registry / "checkpoints").glob("*.pt"))) == 2 + # A rejected promotion must not change active registry state. + bad = evaluation(load_checkpoint(paths[2][0])[1]["checkpointSha256"], .9) + bad["results"][0]["tracks"][0]["metrics"]["hardErrors"] = 1 + paths[2][1].write_text(json.dumps(bad)) + state_before = (registry / "registry.json").read_bytes() + with pytest.raises(ValueError, match="hard validation"): + register_checkpoint(registry, *paths[2]) + assert (registry / "registry.json").read_bytes() == state_before + + +def test_registry_rejects_tampered_active_artifact(tmp_path): + registry = tmp_path / "registry" + (registry / "checkpoints").mkdir(parents=True) + digest = "a" * 64 + (registry / "registry.json").write_text(json.dumps({"active": digest})) + (registry / "checkpoints" / f"{digest}.pt").write_bytes(b"wrong content") + with pytest.raises(ValueError, match="content hash"): + active_checkpoint(registry) + assert registry_status(registry)["status"] == "invalid" + + +def test_uninitialized_registry_has_honest_status(tmp_path): + assert registry_status(tmp_path / "missing") == { + "status": "empty", "active": None, "modelCount": 0, "automaticTraining": False, + } diff --git a/.codex/skills/beat-saber-mapping/tests/test_rl_training_correctness.py b/.codex/skills/beat-saber-mapping/tests/test_rl_training_correctness.py new file mode 100644 index 0000000..df361ee --- /dev/null +++ b/.codex/skills/beat-saber-mapping/tests/test_rl_training_correctness.py @@ -0,0 +1,167 @@ +"""Small deterministic RL regressions; no downloads or long training run.""" + +import json + +import numpy as np +import pytest + +torch = pytest.importorskip("torch") + +from rl.checkpoints import load_checkpoint, save_checkpoint +from rl.environment import BeatSaberEnv, decode_hand_action +from rl.evaluate import chart_metrics, evaluate_checkpoints +from rl.features import load_analysis_features +from rl.models import ActorCriticPolicy +from rl.policy_generator import RLMapGenerator +from rl.train_custom_tracks import extract_track_features, train_on_custom_library +from rl.train_ppo import RolloutBuffer, train_ppo +from test_rl_features import write_analysis +from kinematics import cross_hand_finding, vision_blocking_double + + +def test_terminal_last_slot_does_not_bootstrap_reset_episode_value(): + buffer = RolloutBuffer(1, 69) + buffer.ptr = 1 + buffer.rewards[0] = 3 + buffer.values[0] = 2 + buffer.dones[0] = 1 + buffer.compute_gae(last_value=100, gamma=.9) + assert buffer.returns[0].item() == pytest.approx(3) + assert torch.isfinite(buffer.advantages).all() + + +def test_gae_cuts_internal_episode_boundary_but_bootstraps_nonterminal_tail(): + buffer = RolloutBuffer(3, 69) + buffer.ptr = 3 + buffer.rewards[:] = torch.tensor([1, 2, 5]) + buffer.values[:] = torch.tensor([0, 0, 0]) + buffer.dones[:] = torch.tensor([0, 1, 0]) + buffer.compute_gae(last_value=10, gamma=.5, gae_lambda=1) + assert buffer.returns.tolist() == pytest.approx([2, 2, 10]) + + +def test_inference_requires_checkpoint_and_rejects_corrupt_weights(tmp_path): + with pytest.raises(ValueError, match="explicit trained checkpoint"): + RLMapGenerator() + with pytest.raises(FileNotFoundError): + RLMapGenerator(model_path=tmp_path / "missing.pt") + path = tmp_path / "broken.pt" + path.write_bytes(b"not a checkpoint") + with pytest.raises(ValueError, match="Invalid RL checkpoint"): + RLMapGenerator(model_path=path) + + +def test_checkpoint_hash_and_feature_provenance(tmp_path): + policy = ActorCriticPolicy(hidden_dim=32) + path = tmp_path / "policy.pt" + save_checkpoint(policy, path, {"seed": 4, "trainingSourceSha256": ["a" * 64]}) + loaded, metadata = load_checkpoint(path) + assert metadata["seed"] == 4 + assert len(metadata["checkpointSha256"]) == 64 + assert all(torch.equal(tensor, loaded.state_dict()[key]) for key, tensor in policy.state_dict().items()) + bad = policy.state_dict() + bad["encoder.0.weight"][0, 0] = float("nan") + torch.save(bad, path) + with pytest.raises(ValueError, match="non-finite"): + load_checkpoint(path) + + +def test_training_and_inference_load_identical_features(tmp_path): + path = write_analysis(tmp_path / "analysis") + audio, grid, bpm = extract_track_features(path) + bundle = load_analysis_features(path) + assert audio == bundle.audio_features + assert grid == bundle.beat_grid + assert bpm == bundle.bpm + assert audio["sections"][-1] == "chorus" + + +def test_short_ppo_run_stops_at_requested_steps_and_repeats_with_seed(): + states = [] + for _ in range(2): + torch.manual_seed(23) + policy = ActorCriticPolicy(hidden_dim=16) + env = BeatSaberEnv(beat_grid=[0, .25]) + metrics = train_ppo(policy, env, total_timesteps=3, rollout_steps=2, ppo_epochs=1, batch_size=2, seed=23) + assert metrics["timesteps"] == 3 + assert metrics["completed_episodes"] == 1 + assert np.isfinite(metrics["policy_loss"]) + states.append({key: value.clone() for key, value in policy.state_dict().items()}) + assert all(torch.equal(states[0][key], states[1][key]) for key in states[0]) + + +def test_environment_seed_does_not_mutate_numpy_global_rng(): + np.random.seed(321) + expected = np.random.random() + np.random.seed(321) + env = BeatSaberEnv(beat_grid=[0, .25]) + env.reset(seed=77) + assert np.random.random() == expected + assert not any(env.audio_features["stemAvailability"].values()) + + +def test_blue_mask_rejects_every_unsafe_simultaneous_pair(): + env = BeatSaberEnv(beat_grid=[0, .25]) + for red_action in range(1, env.action_dim): + red_pose = decode_hand_action(red_action) + red = {"b": 0, "c": 0, "x": red_pose[0], "y": red_pose[1], "d": red_pose[2]} + mask = env.blue_action_mask(red_action) + assert mask[0] + for blue_action in np.flatnonzero(mask[1:]) + 1: + x, y, direction = decode_hand_action(int(blue_action)) + blue = {"b": 0, "c": 1, "x": x, "y": y, "d": direction} + assert cross_hand_finding(red, blue) is None + assert not vision_blocking_double(red, blue) + + +def test_conditional_sampling_and_ppo_replay_have_identical_log_probabilities(): + torch.manual_seed(42) + env = BeatSaberEnv(beat_grid=[0, .25]) + policy = ActorCriticPolicy(hidden_dim=16) + obs = torch.as_tensor(env.reset()[0]).unsqueeze(0) + mask_red, mask_blue = env.action_masks() + conditional = lambda chosen: torch.as_tensor(env.blue_action_mask(int(chosen.item()), mask_blue)).unsqueeze(0) + action, sampled_log_prob, _, _ = policy.get_action_and_value( + obs, mask_red=torch.as_tensor(mask_red).unsqueeze(0), + mask_blue=torch.as_tensor(mask_blue).unsqueeze(0), blue_mask_fn=conditional, + ) + effective_blue = conditional(action[0]) + assert effective_blue[0, int(action[1].item())] + _, replay_log_prob, _, _ = policy.get_action_and_value( + obs, action=action, mask_red=torch.as_tensor(mask_red).unsqueeze(0), mask_blue=effective_blue, + ) + assert sampled_log_prob.item() == pytest.approx(replay_log_prob.item()) + + +def test_benchmark_uses_held_out_hashes_and_reports_real_validation(tmp_path): + write_analysis(tmp_path / "train", source="a" * 64) + write_analysis(tmp_path / "held-out", source="b" * 64) + manifest = tmp_path / "library.json" + manifest.write_text(json.dumps({"schemaVersion": 1, "tracks": [ + {"id": "train", "analysisDir": "train", "split": "train"}, + {"id": "held-out", "analysisDir": "held-out", "split": "validation"}, + ]})) + policy = ActorCriticPolicy(hidden_dim=16) + for parameter in policy.parameters(): + parameter.data.zero_() # Deterministic idle policy, only for the fixture. + path = tmp_path / "policy.pt" + save_checkpoint(policy, path, {"trainingSourceSha256": ["a" * 64]}) + result = evaluate_checkpoints(manifest, [path], seed=3) + assert result["heldOutVerifiedAgainstRecordedTraining"] + assert result["results"][0]["tracks"][0]["id"] == "held-out" + assert result["results"][0]["tracks"][0]["metrics"]["notes"] == 0 + assert result["results"][0]["tracks"][0]["metrics"]["headsetPlaytest"] == "not_performed" + save_checkpoint(policy, path, {"trainingSourceSha256": ["b" * 64]}) + with pytest.raises(ValueError, match="used to train"): + evaluate_checkpoints(manifest, [path]) + + +def test_structural_benchmark_detects_colliding_notes(tmp_path): + bundle = load_analysis_features(write_analysis(tmp_path / "analysis")) + chart = {"version": "3.3.0", "colorNotes": [ + {"b": 1, "c": 0, "x": 1, "y": 0, "d": 1}, + {"b": 1, "c": 1, "x": 1, "y": 0, "d": 1}, + ]} + result = chart_metrics(chart, bundle, "Expert") + assert result["hardErrors"] > 0 + assert result["nps"] == pytest.approx(.8) diff --git a/.github/workflows/skill-drift.yml b/.github/workflows/skill-drift.yml index 899e9d2..3fd6bf0 100644 --- a/.github/workflows/skill-drift.yml +++ b/.github/workflows/skill-drift.yml @@ -21,7 +21,11 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" - - run: python -m pip install -e ".[dev]" + - run: python -m pip install -e ".[dev,studio,connector]" + - uses: actions/setup-node@v4 + with: + node-version: "22" + - run: node --test tests/connector_upload_retry.test.cjs - run: python -m playwright install --with-deps chromium - run: python -m pytest tests -q -m "not network and not hardware and not codex" diff --git a/.gitignore b/.gitignore index 93dcf1f..5abcc92 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,9 @@ BeatSaberVersion.txt .DS_Store Thumbs.db site/ +!web/assets/demo/song.ogg +data/learning.json +data/model_registry/ +data/worker/ +data/connector/ +data/connector-smoke/ diff --git a/README.md b/README.md index f52ad57..26d69e8 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,18 @@ BeatForge is a local Windows studio console for generating and reviewing five-di The WebMCP Challenge edition was built as a solo project with OpenAI Codex as the only AI coding assistant. In my own testing, MCP access makes generation iteration nearly 10 times faster than my previous manual workflow. I estimate the combined analysis, constraints, and review process produces roughly 100 times better consistency and playable quality. These are project observations, not a controlled industry benchmark. -Live rights-safe demo: . The full FastAPI studio can still be run locally or deployed with `render.yaml`. +Live rights-safe demo: . Run the full FastAPI Studio locally. `render.yaml` now hosts only the isolated authenticated connector gateway; it must not expose the local game-control API. Every run is sample-based, deterministic, and refusal-gated. Uncertain timing returns `needs_anchors`. Missing or unreadable artwork returns `needs_palette`. Same-hand flow conflicts, inward-facing handclaps, saber collisions, arc or chain ownership errors, bomb paths, walls, vision blocks, schema errors, and timing failures block the output. +## Remote connector + +BeatForge includes shared REST and Streamable HTTP MCP operations for generation, QA, previews, section revisions, presets, comparisons and explicit feedback. Durable jobs are processed by an outbound local worker; private corpus assets stay on the PC. Studio can connect to review remote runs and use the editorial controls. + +Start with [hosting and budget setup](docs/HOSTING.md), [authentication](docs/CONNECTOR_AUTH.md), [worker setup](docs/CONNECTOR_WORKER.md), [editorial contracts](docs/CONNECTOR_EDITORIAL.md), and [Meta Muse, GPT/ChatGPT, Claude and Grok setup](docs/clients/README.md). + +Local HTTP MCP and synthetic generation have been tested. Hosted OAuth, individual assistant accounts and public deployment have not. Studio supports direct gateway uploads and Premium generation. Browser OAuth sign-in is still in progress. No hosting credits have been spent. + ## Local setup ```powershell @@ -17,6 +25,15 @@ pip install -e ".[dev]" python skills\beat-saber-mapping\scripts\bootstrap.py --tier core ``` +For a guided Windows setup and a read-only dependency check, use the included launcher: + +```powershell +.\Start-BeatForge.ps1 -Setup -Check +.\Start-BeatForge.ps1 +``` + +The launcher never downloads model weights during its doctor pass. It reports missing optional trackers and the local official corpus separately from the dependencies required to open Studio. + Optional audio models (Beat This, BeatNet+, All-In-One, Demucs) need a working local PyTorch install. If `torch_python.dll` fails to load from the bootstrap `--target` cache, install Torch into the project venv instead of weakening @@ -54,6 +71,20 @@ The optional Codex review key lives in Windows Credential Manager as `BeatForge:codex`. The studio writes it from the Codex setup dialog; it never prints it back. +## Feedback and self-improvement + +Studio records only explicit local feedback against the exact chart hash, audio hash, difficulty, tester, and optional beat range. Presets, A/B preferences, and accepted revisions are exportable from `/api/learning/export` without copying song audio. Suggestions are bounded adjustments derived from repeated feedback; they are shown for approval and never silently retrain or rewrite a map. + +The RL path requires an explicit checkpoint and uses the same sample-aligned analysis features for training, evaluation, and generation. Held-out reports and content hashes are required before a checkpoint can enter `data/model_registry`; promotion is blocked by hard validation failures or quality regressions, and `rollback` restores the prior immutable version. Training reward and structural QA are reported separately from human preferences and headset evidence. + +Run the regression suites with isolated temporary files: + +```powershell +.venv\Scripts\python.exe -m pytest tests -q -m "not corpus and not network and not hardware and not codex" +$env:PYTHONPATH = "skills/beat-saber-mapping/scripts" +.venv\Scripts\python.exe -m pytest skills/beat-saber-mapping/tests -q -m "not corpus" +``` + ## Failure recovery | Studio state | What happened | What to do | diff --git a/Start-BeatForge.ps1 b/Start-BeatForge.ps1 new file mode 100644 index 0000000..6b04fbc --- /dev/null +++ b/Start-BeatForge.ps1 @@ -0,0 +1,30 @@ +param( + [switch]$Setup, + [switch]$Check, + [ValidateRange(1024, 65535)][int]$Port = 8001 +) +$ErrorActionPreference = 'Stop' +$studioRoot = $PSScriptRoot +$studioPython = Join-Path $studioRoot '.venv\Scripts\python.exe' +Push-Location -LiteralPath $studioRoot +try { + if ($Setup) { + if (-not (Test-Path -LiteralPath $studioPython)) { + python -m venv .venv + if ($LASTEXITCODE -ne 0) { throw 'Install Python 3.11 or newer, then retry -Setup.' } + } + & $studioPython -m pip install -e '.[dev,studio]' + if ($LASTEXITCODE -ne 0) { throw 'Dependency installation failed. Check the pip output above.' } + } + if (-not (Test-Path -LiteralPath $studioPython)) { + throw 'The project environment is missing. Run .\Start-BeatForge.ps1 -Setup first.' + } + & $studioPython tools\doctor.py + if ($LASTEXITCODE -ne 0) { throw 'Fix the required dependency checks above before starting Studio.' } + if ($Check) { return } + Write-Host "Open http://127.0.0.1:$Port/ to use BeatForge Studio. Press Ctrl+C to stop." + & $studioPython -m uvicorn beatforge.api:app --host 127.0.0.1 --port $Port + if ($LASTEXITCODE -ne 0) { throw 'Studio stopped with an error. Check the server output above.' } +} finally { + Pop-Location +} diff --git a/docs/CONNECTOR_AUTH.md b/docs/CONNECTOR_AUTH.md new file mode 100644 index 0000000..066caf8 --- /dev/null +++ b/docs/CONNECTOR_AUTH.md @@ -0,0 +1,45 @@ +# Connector authentication + +The connector is a separate application from the privileged local Studio. It implements an OAuth resource server, not an authorization server. Sign-in, consent, client registration and token issuance belong to your configured OAuth provider. No provider account or public deployment has been configured yet. + +Install the connector dependencies with `python -m pip install '.[connector]'`. Start the isolated gateway with `python -m uvicorn beatforge.connector:from_env --factory --host 127.0.0.1 --port 8013`. + +## Hosted OAuth configuration + +Set these environment variables in the gateway service. Example domains are placeholders. + +| Variable | Example or purpose | +| --- | --- | +| `BEATFORGE_OAUTH_ISSUER` | `https://identity.example/`, exactly matching the provider's issuer claim | +| `BEATFORGE_OAUTH_JWKS_URL` | `https://identity.example/.well-known/jwks.json`, a trusted provider key URL | +| `BEATFORGE_OAUTH_RESOURCE` | `https://forge.example/mcp`, also the required access-token audience | +| `BEATFORGE_CONNECTOR_HOSTS` | `forge.example`, comma-separated explicit allowed hosts | +| `BEATFORGE_CONNECTOR_ORIGINS` | `https://studio.example`, comma-separated browser origins | +| `BEATFORGE_CONNECTOR_DB` | A path on persistent storage, such as `/var/data/jobs.sqlite3` | +| `BEATFORGE_REVOKED_SUBJECTS` | Optional JSON array of provider subject IDs denied access, default `[]` | + +The provider must issue RS256 access tokens with `iss`, `aud`, `sub`, `iat` and `exp`. Permissions use the space-separated `scope` claim: `read`, `generate`, `edit`, `feedback`. The server ignores unsupported scopes, including `worker`. Worker authority must be provisioned separately. Give the provider a dedicated API audience; never configure an identity-token client ID as the resource audience. + +Unauthenticated clients discover the issuer through `/.well-known/oauth-protected-resource/mcp` or the root metadata route. Unauthorized responses include a `WWW-Authenticate` challenge pointing to that metadata. Both REST and MCP require authorization on every request. Browser CORS preflight works only for the configured origins. + +The provider must offer authorization-server discovery and authorization code flow with PKCE. Configure each client's redirect URI and client registration at the provider. Dynamic registration or manual client IDs depend on the provider and actual client. This code does not establish that ChatGPT, Claude, Grok or Muse has completed a login flow. + +Account ownership is derived from the trusted issuer and subject together. Token-controlled key URLs are ignored. Key discovery uses the configured JWKS endpoint with a five-second timeout and five-minute key-set cache. Invalid tokens and unavailable keys fail closed. No incoming assistant token is forwarded to a worker or another service. + +Use short-lived access tokens. JWT validation alone does not detect provider-side revocation before token expiry. For immediate local account blocking, add the subject to `BEATFORGE_REVOKED_SUBJECTS` and restart the service. Immediate per-token revocation/introspection is not implemented. Re-run the real client login and revocation acceptance tests after choosing a provider. + +## Private pilot credentials + +For local integration tests or a private pilot, `BEATFORGE_CONNECTOR_TOKENS` accepts a JSON object mapping random tokens to an owner and scopes: + +```json +{"": {"owner": "pilot-account", "scopes": ["read", "edit"]}} +``` + +Generate secrets with a cryptographic random generator and store them in environment secrets, never source control. These credentials are static bearer tokens, not OAuth. Removing a token and restarting revokes it. Use the same owner only when connections deliberately share an account. OAuth owners are separate from pilot owners unless an administrator explicitly provisions that linkage. + +## Validation status + +Automated tests exercise signed RSA tokens, resource discovery, CORS preflight, wrong issuer/audience, expiry, malformed claims, account revocation, JWKS outage, ownership isolation and shared REST/MCP editorial behavior. They use local keys, not a live provider. Actual hosted login, client registration and provider revocation remain unverified. + +References: [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) and [PyJWT usage](https://pyjwt.readthedocs.io/en/latest/usage.html). diff --git a/docs/CONNECTOR_EDITORIAL.md b/docs/CONNECTOR_EDITORIAL.md new file mode 100644 index 0000000..5858f8a --- /dev/null +++ b/docs/CONNECTOR_EDITORIAL.md @@ -0,0 +1,50 @@ +# Shared editorial API + +The remote connector preserves creative briefs in each job request and shares operation handlers between REST and MCP. Its data is account-owned in the gateway SQLite database. The local Studio's existing feedback file is separate and is not silently uploaded or merged. + +| MCP tool | REST operation | Required scope | +| --- | --- | --- | +| `list_jobs` | `GET /api/jobs?limit=20&offset=0` | `read` | +| `get_job` | `GET /api/jobs/{job_id}` | `read` | +| `get_chart_preview` | `GET /api/jobs/{job_id}/preview?difficulty=Hard` | `read` | +| `get_qa` | `GET /api/jobs/{job_id}/qa` | `read` | +| `record_mapping_feedback` | `POST /api/feedback` | `feedback` | +| `record_comparison` | `POST /api/comparisons` | `feedback` | +| `save_preset` | `POST /api/presets` | `edit` | +| `list_presets` | `GET /api/presets` | `read` | +| `get_learning_summary` | `GET /api/learning` | `read` | +| `suggest_mapping_adjustments` | `POST /api/learning/suggestions` | `read` | + +MCP preview responses contain identity, timing, sections, note count and the REST preview path. REST returns the full chart and waveform plus a five-minute audio URL for the editor. Both read the same hash-verified published artifact. Relative links resolve against the gateway origin. QA findings establish structural inspection only, not headset comfort or release clearance. + +Use the authenticated `/openapi.json` route for exact REST schemas. It declares HTTP bearer authentication. MCP tool schemas come from the same request models; MCP object arguments have the wrapper names `feedback`, `comparison`, `preset` or `request`, while REST accepts the object directly as its body. + +## Feedback and comparison + +Feedback requires `jobId`, `difficulty` and `idempotencyKey`, plus a rating, tag or note. Include the person's tester name. Ratings are integer values from 1 to 5 for overall, flow, readability, musicality and variety. Tags use the same local Studio vocabulary. Agents must only record what a person explicitly supplied; neither defaults nor automated QA establish a human rating. + +The server binds records to chart, audio and preview artifact hashes. Clients cannot supply replacement hashes. Retrying identical feedback with the same key returns the original record; changing the payload with that key fails. Comparisons require two different, owned charts with matching audio, tempo, offset and difficulty. Identical charts are rejected. + +Suggestions reuse the local Studio's bounded feedback rules. They need evidence from at least three distinct charts from the selected tester, deduplicate repeated reports, and return a proposed plan without applying it. They do not train or promote an RL model. Hosted training and promotion are not implemented. + +## Presets and retention + +Saving a preset normalizes its musical plan and replaces the same account's preset with the same case-insensitive name. The gateway currently permits up to 1,000 editorial records per account. Authenticated `/api/learning/export` exports only the caller's feedback, comparisons and presets, including their chart hashes. Deletion and retention controls remain unfinished. + +## Current verification + +Tests cover history pagination, cross-account denial for previews and QA, hash-bound feedback, duplicate handling, permission scopes, comparison identity, private presets and insufficient-evidence suggestions. The Studio connection now routes history, presets, explicit feedback, comparisons, suggestions and dataset export through the gateway while connected. Connection changes clear drafts and comparison state; local previews cannot be rated into the remote account. Identical feedback submissions reuse their idempotency key within the connection. A/B preview switching preserves the song position and selected comparison difficulty. Remote section revision now queues through REST/MCP and the Studio; the live browser revision flow has been verified against a localhost gateway. Browser checks verified a real remote preview, preset save/reload, and empty-feedback rejection without inventing human ratings. Browser A/B switching between a real generated parent and revision preserved the 7-second playback position; no preference rating was fabricated. + + +## Remote section revisions + +Call MCP `revise_section` with `job_id` and `revision`, or POST `/api/jobs/{job_id}/revise` with the revision body. Required fields are `idempotencyKey`, `difficulty`, `baseHash`, `startBeat`, and `endBeat`; optional fields are `seed` and `mappingPlan`. The base hash comes from the published preview. Edit scope and ownership are required. The server rejects stale hashes, reversed/out-of-song ranges and variable-BPM charts. The operation returns a separate queued job and preserves its parent. + +Keep the original worker directory. Revisions use that worker's retained analysis and locate a parent map by its complete chart/audio identity; private analysis is never uploaded to the gateway. A different worker without the cache cannot revise the map and reports failure. This is currently a single-worker-per-account workflow, not distributed cache recovery. Older published previews may have revision controls disabled; newly published previews advertise revision eligibility when analysis exists. + +The worker revalidates the parent, regenerates the selected difficulty using the existing choreography and timing projection, and applies the shared half-open section merge. Crossing objects require a wider range. All unselected difficulties and objects outside the range are preserved. The child clears inherited playtests and reviews, then passes complete-package QA before any artifacts are published. Failed validation leaves the parent untouched and publishes no playable child. + +Transport tests exercise a real section generator and validator with an isolated synthetic audio/chart fixture, stale hashes, ownership denial, idempotency, outside-range preservation, unchanged parent hashes and unselected difficulty bytes, cleared human evidence, and a deliberately invalid child that cannot publish. These are automated structural checks, not headset approval. The Studio submits remote revisions to the gateway, polls their status, and opens a completed child only if the person is still previewing its parent. + + +On 2026-09-20, a real browser selected the repository synthetic OGG, uploaded it to a separate local HTTP gateway, and queued Hard generation. The outbound worker completed job `f154782ba3da4193a1d69be586df9ec8`. The browser then revised beats 0–16 into child `09eab72ffe764b4ea466790992270688`, opened the result, and switched A/B at 7 seconds. Disk checks confirmed the parent hash was unchanged, the child chart changed, all content outside the selection was preserved, and child QA had zero errors. Timing was explicitly allowed to remain unconfirmed. The ignored smoke directory contains `browser-workflow-report.json`. This is local end-to-end evidence, not hosted OAuth or assistant-account verification. diff --git a/docs/CONNECTOR_RECOVERY.md b/docs/CONNECTOR_RECOVERY.md new file mode 100644 index 0000000..dd45164 --- /dev/null +++ b/docs/CONNECTOR_RECOVERY.md @@ -0,0 +1,35 @@ +# Recover an interrupted audio upload + +Normal audio and artifact transfers have a five-minute deadline. They remove partial files on timeout or disconnect. Expired pending audio reservations are reclaimed when the next upload is reserved. Completed audio is retained for generation and revisions. + +A process crash can bypass normal cleanup and leave an audio reservation in `writing`. Use the offline maintenance command for this case. It is intentionally unavailable to assistant tools and HTTP clients. + +1. Stop **every gateway process** using the database. A SQLite lock cannot stop a process that already holds an open upload file. Keep the gateway stopped throughout recovery. +2. Back up the database and its adjacent uploads/artifacts directories consistently. +3. Review the dry-run counts: + + ```sh + python -m beatforge.connector_maintenance --database /var/data/beatforge/jobs.sqlite3 + ``` + +4. Apply only after verifying the target database and stopped service: + + ```sh + python -m beatforge.connector_maintenance --database /var/data/beatforge/jobs.sqlite3 --apply --gateway-stopped + ``` + +5. Restart the gateway. Retry an unexpired reservation, or reserve a new upload if it expired. Studio automatically renews its expired cached reservation. + +The command validates all selected paths before changing files. It removes only partial bytes belonging to reservations in `writing`, then resets those reservations to `pending`. Ready uploads, published artifacts, jobs, presets and feedback are untouched. Repeating recovery is safe after interruption. A path-validation or filesystem error stops recovery; inspect it rather than manually removing unrelated files. + +## Reclaim obsolete worker attempts + +With the gateway stopped and a consistent backup made, preview artifacts that belong to abandoned worker attempts: + +```sh +python -m beatforge.connector_maintenance --database /var/data/beatforge/jobs.sqlite3 --obsolete-artifacts +``` + +The report includes artifact count and reserved bytes. Add `--apply --gateway-stopped` to remove only that obsolete set. Every artifact referenced by any job result is protected, as is every artifact belonging to the current lease of a running job, even if that lease has expired. Invalid metadata or paths stop cleanup. Run it again after the job queue has resolved expired attempts if those bytes still need reclaiming. + +These commands are recovery, not completed-data retention. Old ready uploads and completed maps still require a separate retention policy before sustained hosted use. No live production recovery has been performed; automated tests cover dry-run behavior, preservation of ready audio, published results and current leases, retry, repeat execution and validation failure before removal. diff --git a/docs/CONNECTOR_RELEASE_STATUS.md b/docs/CONNECTOR_RELEASE_STATUS.md new file mode 100644 index 0000000..64c951d --- /dev/null +++ b/docs/CONNECTOR_RELEASE_STATUS.md @@ -0,0 +1,25 @@ +# Connector release evidence + +Status: implementation available in draft PR #1; public deployment and assistant account compatibility are not verified. Last reviewed 2026-09-20. + +| Requirement | Evidence | Remaining verification | +| --- | --- | --- | +| Shared MCP and REST operations | `connector.py`, operations tests, real HTTP SDK check in `tools/check_connector.py` | Repeat against hosted HTTPS endpoint | +| Authentication, scopes and ownership | RSA JWT/static-token tests, resource discovery, cross-owner denials | Configure identity provider; complete real client login and revocation checks | +| Durable generation and local worker | SQLite leases/idempotency, worker recovery tests; real synthetic browser generation | Measure hosted gateway memory, disk and egress | +| Briefs, plans, previews and QA | Real synthetic generation and synchronized browser preview | Human musical/headset assessment remains separate | +| Safe section revision and A/B | Real child generation, unchanged parent, preserved outside section, zero QA errors; browser A/B retained position | Requires original worker cache | +| Presets and explicit feedback | Owner-bound persistence tests and browser preset save | No human ratings or playtest clearance have been fabricated | +| Human Studio presentation | Transparent neon logo; gateway connection form reviewed at 390, 768 and 1440 pixels, no horizontal page overflow | Connected feedback/comparison controls reviewed at all three widths; no ratings submitted | +| Existing local behavior | GitHub app-tests and portable skill checks succeeded for `d30e328`; local non-browser suite previously passed | Continue regression checks on subsequent changes | +| Render/Vercel readiness | Isolated lightweight gateway, persistent disk blueprint, static Studio build, setup instructions | Accounts/projects, credit eligibility and selected costs must be checked before provisioning | +| Meta Muse, GPT/ChatGPT, Claude and Grok | Individual setup guides and common acceptance procedure in `clients/` | All four actual account workflows remain unverified | + +## Open release work + +- Select and configure the OAuth provider and hosting projects. Studio currently supports an in-memory private token session; browser OAuth sign-in is not implemented. +- Exercise each named assistant with the same synthetic generation/revision workflow after the gateway is reachable over HTTPS. SDK transport checks alone do not establish these integrations. +- Choose retention for completed data before a sustained hosted pilot. Expired pending reservations are reclaimed during new reservations, and stalled transfers time out with retry support. Interrupted audio and obsolete worker artifacts have offline dry-run recovery commands that preserve completed results and current leases; see `CONNECTOR_RECOVERY.md`. +- Browser expiry recovery passed on 2026-09-20 against a local test gateway issuing two-second signed tickets: opening the existing synthetic revision and pressing Play after expiry requested a fresh ticket and started playback. The temporary gateway was stopped afterward. This does not establish hosted or assistant-account compatibility. + +No cloud resources were provisioned or credits spent. See `CONNECTOR_WORKER.md` and `CONNECTOR_EDITORIAL.md` for local workflow evidence and `clients/README.md` for the account verification procedure. diff --git a/docs/CONNECTOR_WORKER.md b/docs/CONNECTOR_WORKER.md new file mode 100644 index 0000000..bf17927 --- /dev/null +++ b/docs/CONNECTOR_WORKER.md @@ -0,0 +1,57 @@ +# Local connector worker + +The gateway stores account-owned audio and durable job records. The worker polls over outbound HTTP requests, downloads only its leased job's audio, verifies its hash and runs the existing premium mapping pipeline locally. It does not install maps, launch Beat Saber or accept shell commands or filesystem paths from an assistant. + +This is an implementation in progress. Valid generated maps now transfer back to the gateway with audio, QA and per-difficulty preview JSON. Timing-anchor continuation and the hosted editorial UI are not wired yet. A completed queue entry means its pipeline attempt finished; inspect `result.status` for a request for anchors, corpus setup, palette approval, invalid output or a playtest candidate. It never means human approval. + +## Run a private pilot + +Install `.[connector,studio]` in the repository's virtual environment. The worker needs the same local corpus and audio tools as the local Studio. Provision a random worker token in the gateway's `BEATFORGE_CONNECTOR_TOKENS` secret with scope `worker` and the same owner as the intended account. Do not give that token to an assistant. OAuth accounts use a namespaced owner derived from issuer and subject; provisioning must deliberately match that account, not merely a display name. + +Set the worker's `BEATFORGE_WORKER_TOKEN` environment variable, then run: + +```powershell +.venv\Scripts\python.exe -m beatforge.connector_worker --gateway http://127.0.0.1:8013 --work-dir data/worker +``` + +Use HTTPS outside localhost. `--once` claims at most one job and exits. Store the working directory on a disk with room for audio analysis and generated maps. Outputs are retained for inspection; automatic local retention cleanup is not implemented. + +## Request flow + +1. Call `create_upload_session` through MCP, or `POST /api/uploads` with `filename` and exact `size` in bytes. +2. Send raw audio bytes to the returned `uploadPath` using `PUT` and the account's authorization header. Audio is not embedded in MCP messages. Each audio transfer has a five-minute deadline; HTTP 408 removes partial bytes and releases the writer so the same unexpired reservation can be retried. Use `get_upload` or `GET /api/uploads/{id}` to confirm `ready` and its SHA-256 hash. +3. Call `start_generation` through MCP with a `generation` object, or `POST /api/jobs` with the object as its body. Include `uploadId`, `idempotencyKey`, `title` and optional `artist`, `mapper`, `seed`, `difficulties`, `mappingPlan`, `allowUnconfirmed`. Keep `allowUnconfirmed` false unless the person explicitly accepts unverified timing. +4. Read `get_job` or `GET /api/jobs/{id}` for state and result. Submission returns `workerOnline`; offline jobs remain queued. Cancel with `cancel_job` or `POST /api/jobs/{id}/cancel`. +5. Call `get_artifacts` or `GET /api/jobs/{id}/artifacts`. Each output includes a private five-minute `downloadUrl` and a persistent `downloadPath` requiring account authorization. Resolve relative paths against the gateway origin. Request fresh links after expiry or a gateway restart. Download tickets authorize one specific artifact; treat them as secrets. Configure the deployment and reverse proxy to omit query strings from access logs, or disable access logging with Uvicorn's `--no-access-log` option. + +An idempotency key can be retried with the same normalized request. Reusing it for a different request fails. At most two queued/running jobs per account are allowed. Uploads have a 64 MiB per-file limit, 256 MiB reserved account storage limit and 32 reservations per account. An unfinished reservation expires after one hour. Expired pending reservations are reclaimed atomically when a new upload is reserved. Ready uploads and writing reservations remain counted and preserved; retention of completed data and recovery after a process crash still require operator action. The gateway validates extensions and byte counts; the local decoder validates actual audio contents. + +## Lease behavior + +`POST /api/worker/claim` both announces worker presence and claims an account-owned job. A worker is considered online for 90 seconds after its last claim or heartbeat. Leases last 60 seconds, and the worker heartbeats every 15 seconds. Three expired attempts exhaust retries. Cancellation invalidates the lease immediately; an old worker cannot submit a result after cancellation or reassignment. + +Audio download, heartbeat and finish requests require both worker authentication and `X-BeatForge-Lease`. A heartbeat failure stops local computation. The server accepts a constrained result status and message, never pipeline logs or private corpus paths. Authenticated users cannot invoke worker operations without the separately provisioned scope. + +The continuous worker reconnects after transport errors, HTTP 408/425/429 and server failures, with delays of 2, 4, 8, 16, 32 and at most 60 seconds. A successful poll resets the backoff. Authentication and other permanent HTTP errors stop the process; repair configuration before restarting. `--once` propagates failures without retrying. Transient transfer errors leave the job lease recoverable instead of prematurely recording a failed job. If a completion response is lost after the gateway commits it, the worker does not overwrite that result. Reclaiming an expired lease still respects the gateway's three-attempt limit. + +## Evidence + +Artifacts are immutable within a worker lease and count toward a separate 256 MiB account quota. The server only publishes the manifest attached to a completed job. Cancelled and stale attempts cannot publish. Download routes recheck the completed job's published manifest. Map ZIPs contain only referenced gameplay, audio and cover files, not `_beatforge` metadata or unrelated files. Public preview JSON excludes local provenance; public QA retains finding codes and beat positions while omitting diagnostic text that may contain machine paths. + +Connector tests exercise streamed size rejection, ownership, immutable completed uploads, quota enforcement, atomic concurrent claims, bounded retries, cancellation, worker input hashing and forwarding the normalized musical brief. Worker pipeline tests substitute a mapper function to isolate the transport contract. Artifact tests publish the repository's real synthetic map fixture through the worker and gateway, then retrieve previews and verify signed-link tampering, expiry and ownership restrictions. + +On 2026-09-20, `python tools/smoke_connector.py` also ran the real pipeline through an in-process gateway with the synthetic 30-second track and an isolated copy of the local official corpus. The fresh Hard chart had 32 notes, zero bombs, zero QA errors and one warning. Generation plus upload and retrieval took 61.08 seconds. All four artifacts downloaded successfully: map ZIP, audio, Hard preview and QA. Timing remained explicitly unverified; the test deliberately allowed unconfirmed timing and supplied no human playtest evidence. This is local integration evidence, not a hosted deployment or a verified assistant login. The script retains its report, local diagnostics and artifacts under the ignored `data/connector-smoke` directory. + +The smoke run exposed a missing UnityPy dependency. The Studio extra and dependency doctor now include the skill's pinned UnityPy version. The script copies the corpus with SQLite's read-only source connection before indexing, so the original database is not modified by the test. + +## Real HTTP MCP check + +Set `BEATFORGE_CHECK_TOKEN` to a read-scoped account token in the terminal environment, then run: + +```powershell +.venv\Scripts\python.exe tools/check_connector.py --gateway https://YOUR-GATEWAY --job YOUR_JOB_ID +``` + +Omit `--job` for discovery and mapping-plan checks only. The script uses the official Python MCP SDK, compares the same account's MCP and REST responses, and streams published artifacts through authenticated download routes to verify sizes and SHA-256 hashes. It does not generate maps or write feedback. Output excludes tokens and signed download URLs. + +Verified on 2026-09-20 against a live localhost gateway: initialization, 17 discovered tools, equal MCP/REST plans, equal job and QA data, and all four synthetic output downloads with matching hashes. This proves the HTTP MCP transport for that local configuration. It does not prove Meta Muse, Grok, ChatGPT or Claude account setup, hosted OAuth, public deployment, or credit eligibility. diff --git a/docs/HOSTING.md b/docs/HOSTING.md new file mode 100644 index 0000000..4dd3d23 --- /dev/null +++ b/docs/HOSTING.md @@ -0,0 +1,40 @@ +# Hosting BeatForge + +These files prepare deployments; they do not create resources. No credits have been spent and no hosted client login has been verified. The Vercel build includes the synthetic Studio demo and a private token connection panel for remote run previews and ZIP links. Feedback, comparisons and presets use the gateway while connected. Section revisions use the original worker cache. Studio now uploads audio directly and queues Premium generation; browser OAuth sign-in is still pending. + +## Render gateway + +Use the repository root containing `render.yaml` as the Blueprint root. The blueprint runs only `beatforge.connector:from_env`, with one process and a 1 GB persistent disk. It must never run `beatforge.api:app`: that API controls the local game, installation and credentials. `requirements-gateway.txt` excludes audio analysis, Torch and the official corpus. Source stays in the repository checkout because the shared mapping-plan module uses the checked-in portable skill. + +Before provisioning, review the selected compute and disk prices, credit eligibility and expiration in your Render account. The $50 credit is a ceiling for this project, not a promise of a particular runtime. Set account billing alerts and review usage; this blueprint does not enforce a spending cap. Auto-deploy is disabled. There is no autoscaling or cloud generation worker. + +Supply these environment variables through Render's secret/settings UI: + +| Variable | Value | +| --- | --- | +| `BEATFORGE_CONNECTOR_HOSTS` | Actual gateway hostname, without scheme or path; include any custom hostname explicitly. No wildcard. | +| `BEATFORGE_CONNECTOR_ORIGINS` | Exact Studio HTTPS origin; comma-separated only if more are intentionally allowed. | +| `BEATFORGE_CONNECTOR_TOKENS` | Private JSON token registry described in CONNECTOR_AUTH.md; worker token owner must match the user's owner. Never place this JSON in Vercel or source control. | +| `BEATFORGE_OAUTH_ISSUER` | Configured OAuth provider issuer. | +| `BEATFORGE_OAUTH_JWKS_URL` | That provider's trusted HTTPS signing-key endpoint. | +| `BEATFORGE_OAUTH_RESOURCE` | `https://YOUR-GATEWAY/mcp`, matching access-token audience. | + +For a private token-only pilot, leave all three OAuth settings empty. OAuth client registration, consent and token issuance belong to the provider; the gateway does not implement an authorization server. See CONNECTOR_AUTH.md for required claims and limitations. + +The configured disk holds SQLite, uploads and published artifacts together. Back up all three consistently before changes. Do not scale this SQLite deployment to multiple instances. Short-lived artifact links expire after a process restart; request new links. Access logs are disabled to avoid recording bearer download tickets; review platform/proxy logging separately. Per-account quotas and an atomic 512 MiB global upload/artifact reservation limit are enforced. Each SQLite connection limits new metadata growth to 64 MiB, leaving room on the 1 GB disk for journals and operations. REST/MCP JSON requests are capped at 1 MiB before parsing, with a 30-second body deadline. These controls bound application storage, not provider billing or operator-added files. New upload reservations reclaim expired pending reservations atomically. Retention of ready uploads/artifacts and recovery of writing reservations after process crashes remain release work; those exhausted reservations require operator action. Do not open this pilot to unrestricted sign-ups. + +Run the outbound worker on your PC using CONNECTOR_WORKER.md. Keep the official corpus and private checkpoints there. Check `/health`, then authenticated capabilities and a real upload/generation/download cycle. A healthy gateway does not prove that a worker is online. + +## Vercel Studio + +Use the same repository root. `vercel.json` selects Other, builds with `node tools/build_static_demo.mjs`, and publishes only `site`. The build needs Node but no npm dependencies or Python. Never set the output directory to the repository root: it contains private development and worker data. Only the synthetic demo assets in `web/assets` are copied. + +Review your Vercel plan and credit eligibility before creating the project. Reserve the stated $20 for eligible usage, configure spending controls available to your account, and avoid functions for audio processing. The static site contains no embedded API tokens. Users can enter a token in the connection panel; it stays in memory for the current tab, with no browser-storage persistence. Use the exact gateway origin and allow the Studio origin in gateway CORS. Tokens are never forwarded to redirects. Disconnect clears remote playback and download links. + +Local browser verification used the real synthetic worker output through a separate HTTP gateway on port 8013 and Studio on port 8014: authenticated history loaded, the creative plan populated, a 30-second audio/chart preview played, a signed ZIP link was retrieved, and disconnect cleared remote data. This was a read-only synthetic token, not a hosted OAuth/client test. Remote previews carry a separate remote job ID. Editorial controls select the matching workspace and refuse to rate a local preview into a remote account; unsupported remote revision handlers cannot write to the local service. + +## Verification status + +Local checks exercise gateway import isolation, authenticated REST and connector contracts. The Node and Python static builds must produce identical HTML and assets. Neither local checks nor a valid blueprint prove Render provisioning, Vercel deployment, OAuth consent, budget enforcement or compatibility inside an assistant client. + +Configuration references checked on 2026-09-20: [Render Blueprint specification](https://render.com/docs/blueprint-spec), [persistent disks](https://render.com/docs/disks), and [Vercel configuration](https://vercel.com/docs/project-configuration/vercel-json). Recheck account-specific prices before provisioning. diff --git a/docs/IMPROVEMENT_PLAN.md b/docs/IMPROVEMENT_PLAN.md new file mode 100644 index 0000000..3cfa2ec --- /dev/null +++ b/docs/IMPROVEMENT_PLAN.md @@ -0,0 +1,77 @@ +# BeatForge improvement plan + +This implementation follows the repository review at `5c0cfb9`. The aim is a studio where creative direction changes the generated chart, the result can be heard and inspected, and a selected passage can be revised without losing the rest of the work. + +## 1. Reliable generation and recovery + +- Repair the browser metadata field contract and both supported timing-anchor formats. +- Require readable, passing QA before a pack is eligible for installation. Preserve a clearly labeled unverified timing workflow without treating structural errors as acceptable. +- Make later failed playtests supersede earlier matching clears. +- Preserve job history, identify interrupted jobs after restart, and offer retry and cancellation. +- Verify these behaviors with real API requests and isolated installation doubles. + +## 2. Creative direction and musical structure + +- Introduce a versioned mapping plan shared by the browser, API, CLI, generator, and provenance. +- Support intensity, density, instrument emphasis, style, verse/chorus contrast, and optional bombs/walls. +- Interpret a documented set of brief phrases deterministically, display the applied settings, and never pretend unrestricted prose understanding. +- Improve phrase consistency and rank safe choreography candidates using musical and corpus evidence. +- Verify that the same seed and plan reproduce output, that different plans change intended behavior, and that hard constraints remain intact. + +## 3. Inspect and revise maps + +- Add an audio-synchronized perspective chart preview, waveform, difficulty selection, seeking, playback speed, section markers, and QA markers. +- Load actual generated map objects through a confined preview API. +- Regenerate a selected beat range into a separate revision, preserve objects outside that range, revalidate seams and the complete result, and retain the original job. +- Provide a real synthetic sample with audio and chart data for local and static previews. Clearly distinguish a sample from the full generation pipeline. +- Verify the browser controls, audio synchronization, actual note data, and revision preservation. + +## 4. Correct and evaluate learning + +- Share beat/sample-aligned feature extraction between training and inference. Use actual section labels and available stem energies, record missing features, and remove invented stems and the fixed 120 BPM clock. +- Require valid inference checkpoints and record their identity. +- Correct terminal bootstrapping in PPO and make training reproducible. +- Add held-out evaluation that reports map quality separately from training loss and reward. +- Verify feature timing, section alignment, checkpoint errors, terminal boundaries, and benchmark output with deterministic fixtures. + +## 5. Setup, verification, and delivery + +- Provide a local launcher and actionable dependency checks. +- Keep the canonical and portable skill copies synchronized. +- Run relevant application and mapping suites, a local Studio browser pass, and generated-pack validation. +- Record test results and limitations. A visual preview or automated validator cannot establish headset comfort or a human clear. +- Deliver the updated repository and instructions for starting it locally. Remote publishing is a separate action from local implementation. + +## 6. A measured self-improvement loop + +User expansion: make BeatForge self-improving and add features that improve the experience. This extends the active implementation goal. + +- Save explicit feedback against the exact chart hash, difficulty, and beat range: timing, flow, readability, repetition, intensity, and freeform headset notes. +- Record A/B preferences between two maps of the same audio and difficulty. Keep an unmodified baseline and retain failed experiments for diagnosis. +- Treat accepted section revisions as examples of user preference. Export a local dataset with versioned provenance and without copying song audio. +- Add a model registry with benchmark results, active checkpoint, prior checkpoint, and rollback. Promotion requires matching held-out benchmark identity, no hard failures, and improvement on the selected quality metric without defined regressions. +- Keep training results, structural validation, and human preference scores separate. The app must never invent ratings, human clears, or claim that a promoted checkpoint has been played. +- Provide a visible improvement dashboard explaining evidence collected, outstanding evaluations, and the next useful action. Training and promotion remain explicit operations with visible results. + +## 7. Faster everyday use + +- Save and reuse personal creative presets, including instrument focus, density, style, and accessibility choices. +- Compare two revisions in the preview at the same audio position and difficulty, then save a preference. +- Show actionable QA findings with beat navigation and retain section feedback alongside revisions. +- Restore the last selected local job after refresh, show interrupted work clearly, and allow recovery without re-uploading audio. +- Expose useful preview, feedback, and comparison operations through typed WebMCP tools with the same validation as the visible controls. + +## Progress + +- [x] Review current repository and create implementation branch. +- [x] Reliable generation and recovery (API lifecycle coverage and durable retry/cancel state). +- [x] Creative controls and musical structure (versioned plans, deterministic brief interpretation, candidate ranking). +- [x] Real map preview and section revisions (audio clock, waveform, QA navigation, isolated range regeneration). +- [x] Learning correctness and evaluation (sample-aligned features, masked PPO, held-out evaluation, checkpoint registry). +- [x] Setup, integration, regression checks, and delivery audit (launcher, doctor, portable lock, browser and API suites). +- [x] Feedback dataset, evaluation-gated model registry, and improvement dashboard. +- [x] Personal presets and comparison workflow. + +The software-side gates are complete. A real headset session remains a human release activity: sight-read each selected difficulty at full and slow speed, inspect comfort and reach, and record that evidence through the Studio before treating a map as cleared. + +The goal remains active while any required implementation or verification is incomplete. diff --git a/docs/LOGO_ASSET.md b/docs/LOGO_ASSET.md new file mode 100644 index 0000000..7a8f70a --- /dev/null +++ b/docs/LOGO_ASSET.md @@ -0,0 +1,7 @@ +# Transparent BeatForge logo + +The Studio uses `web/assets/beat-forge-logo.png`, an RGBA image with transparent background and a small red/blue neon glow. The old radial CSS mask and screen blend were removed. The static demo was rebuilt and the result checked in the browser against the charcoal panel. + +Edited with the built-in image-generation tool. Final prompt: + +> Extract the original BEAT FORGE neon logo onto actual transparency. Deliver a transparent-background RGBA PNG with an alpha channel, alpha=0 outside the neon lettering and its small outer glow. The previous attempt drew checkerboard pixels into an opaque RGB image; that is NOT transparency and must not be repeated. Use actual background removal/transparency output, not a visual simulation. NO checkerboard pixels, NO white, gray or black backdrop. Keep original thin italic letter shape exactly: red BEAT above blue FORGE. Preserve only lettering plus a tight, subtle semi-transparent colored halo. This is an existing UI asset cleanup, not logo redesign. Canvas transparent everywhere else. Real alpha transparency is essential. diff --git a/docs/UNIVERSAL_MCP_PLAN.md b/docs/UNIVERSAL_MCP_PLAN.md new file mode 100644 index 0000000..e6704d2 --- /dev/null +++ b/docs/UNIVERSAL_MCP_PLAN.md @@ -0,0 +1,69 @@ +# BeatForge across AI assistants + +Research date: 2026-09-20. This is a proposed architecture, not a deployed service or a claim of tested client compatibility. + +## Decision + +Build one authenticated service with Streamable HTTP MCP at `/mcp` and a documented REST/OpenAPI interface. Keep browser WebMCP as a third adapter to the same application operations. Use Vercel for the website, Render for the gateway and durable job queue, and an outbound-connected local worker for expensive audio processing and RL. The existing local Studio should not be exposed directly to the internet. + +## Client compatibility + +- ChatGPT: remote MCP app with OAuth and account-dependent developer-mode availability. OpenAI Responses API also supports remote MCP. Public directory distribution is a separate submission process. +- Claude: remote MCP custom connector, plus API connector where needed. +- Grok: custom remote MCP connector and remote MCP API tools. +- Meta Muse consumer app: documented custom connector creation from service API information. Provide a scoped REST API and connector setup instructions. Do not equate this with a verified native MCP URL registration workflow. +- Muse Code: remote MCP settings support credentials/OAuth. Developer-preview plugin manifests currently do not carry credential fields; authenticated access should use settings rather than embedding a token in a plugin. + +## Hosting and budget + +Treat the stated $50 Render and $20 Vercel as separate hosting credits, subject to redemption terms and expiry. They do not establish any model-inference allowance. + +Target a small always-on Render gateway. Published entry compute pricing surfaced at approximately $7/month; $50 / $7 is about seven months of compute alone. Storage, egress, workspace charges, credit eligibility and actual selected plan must be checked before provisioning. Do not put Torch, stem separation or RL training in that small process. + +Use Vercel for static pages, sign-in redirects, upload UI and preview. Avoid running song generation inside a function. Upload song bytes directly to authenticated storage or the gateway, not through MCP messages. Reserve the $20 for eligible usage; do not assume promotional credits pay subscription fees. + +Begin with one Render process and durable SQLite on an attached paid disk for a single gateway instance. Separate job metadata from audio storage and apply retention. Move to a managed database/object store if multi-instance operation or traffic justifies it. + +The PC worker authenticates over outbound HTTPS and leases jobs. The PC must be online for private generation; return worker-offline status immediately when it is unavailable. Keep private official-corpus assets and checkpoints local. Cloud users see only their own authorized artifacts. + +## Implementation sequence + +1. Extract application operations from browser state and FastAPI handlers. Define typed inputs/results shared by REST, MCP and WebMCP. +2. Implement OAuth discovery, audience-bound token validation, project ownership checks and scopes such as read, generate and feedback. A job ID alone must never authorize access. Use an established OAuth provider/library. +3. Add MCP tools: capabilities, create upload session, resolve mapping plan, start generation, inspect job, cancel job, read QA, revise section, get preview/download links, and record explicit feedback. Long operations return a job ID promptly; support idempotent submission. +4. Add a durable queue with leases, heartbeats, attempt limits and restart recovery. A worker uses constrained operations, never arbitrary model-supplied shell commands or filesystem paths. +5. Deploy the gateway separately from the local Studio. Do not expose credential management, game launch or automatic local installation as public cloud tools. Add request/size/concurrency limits and per-user quotas before enabling writes. +6. Connect Vercel UI to the gateway through authenticated requests and an explicit CORS allowlist. Keep secrets server-side, and use short-lived artifact URLs. Build a public read-only synthetic demo before opening paid generation. +7. Supply four setup guides, including Muse custom-connector instructions and Muse Code settings. Test each actual client; do not infer all-client compatibility from SDK tests. + +## Acceptance + +### Human Studio design + +Visual quality is a release requirement alongside connector functionality. Preserve the existing BeatForge logo, red and blue chart identity, creative briefs and editorial controls. The Studio should feel like a music workspace, with the audio and chart preview as the main review area. + +- Use consistent spacing, typography, borders and restrained accent colors. Give primary actions clear emphasis; keep technical configuration behind progressive disclosure. +- Make the sequence easy to follow: choose a track, describe the map, generate, listen and inspect, revise, then export. Let people return to the brief without losing preview position or work. +- Keep waveform, playback controls, difficulty selection, section boundaries and revision tools together. Distinguish the selected section and changes in a revision without relying on color alone. +- Show useful empty, loading, offline, failed and completed states. Never display decorative progress or fake connection status. An assistant-created project must open in the same editorial workspace with its actual brief, artifacts and QA. +- Provide visible keyboard focus, readable contrast, adequately sized controls and reduced-motion support. Support phone, tablet and desktop layouts without horizontal page overflow. +- Review rendered screenshots at 390, 768 and 1440 pixels wide. Exercise upload, demo playback, section selection, revision and recovery in the browser. Passing backend tests does not establish visual quality. + +Initial Studio visual review passed at phone, tablet and desktop widths. Remote upload, generation, preview, section revision and A/B switching have also been exercised in the browser. Final visual regression of the complete connector UI remains part of release review. + +For every client, authenticate, discover tools, submit the same synthetic test job, observe progress, retrieve identical job/QA data and a working preview/download link. Verify token revocation, cross-user denial, duplicate requests, worker disconnect/restart, cancellation, expired downloads and exhausted quotas. No automated test may invent human ratings or headset clearance. Measure real memory, storage and bandwidth before estimating paid cloud generation capacity. + +## Sources + +- Meta Muse custom connectors: https://www.meta.com/help/artificial-intelligence/1687253048996149/ +- Muse Code MCP: https://meta-models.github.io/muse-code-sdk/next/guides/extend/mcp-servers/ +- Muse Code plugins: https://meta-models.github.io/muse-code-sdk/next/guides/plugins/ +- OpenAI remote MCP: https://developers.openai.com/api/docs/guides/tools-connectors-mcp +- OpenAI plugin authentication: https://developers.openai.com/plugins/build/auth +- Claude remote MCP: https://platform.claude.com/docs/en/agents-and-tools/remote-mcp-servers +- Grok remote MCP: https://docs.x.ai/developers/tools/remote-mcp +- Grok custom connectors: https://docs.x.ai/grok/connectors/custom-mcp-tunneling +- Render pricing and disks: https://render.com/pricing and https://render.com/docs/disks +- Vercel function limits: https://vercel.com/docs/functions/limitations + +The initial repository review found only browser-bound WebMCP and the local API. The current worktree now implements the isolated MCP/REST gateway, scoped authentication, durable queue, outbound worker and remote editorial UI. See CONNECTOR_WORKER.md and CONNECTOR_EDITORIAL.md for measured local verification. Hosted OAuth and assistant-account tests remain unverified. No hosting resources have been purchased or deployed. diff --git a/docs/clients/CLAUDE.md b/docs/clients/CLAUDE.md new file mode 100644 index 0000000..f2c0169 --- /dev/null +++ b/docs/clients/CLAUDE.md @@ -0,0 +1,30 @@ +# Claude + +For a Claude account offering custom connectors, register your hosted `/mcp` URL and complete its authentication setup. Verify actual discovery and account ownership before enabling writes. + +For the Messages API, use the documented MCP beta with a server entry and a separate toolset. Construct these values server-side: + +```python +import os + +mcp_servers = [{ + "type": "url", + "name": "beatforge", + "url": os.environ["BEATFORGE_MCP_URL"], + "authorization_token": os.environ["BEATFORGE_ACCESS_TOKEN"], +}] +tools = [{ + "type": "mcp_toolset", + "mcp_server_name": "beatforge", + "default_config": {"enabled": False}, + "configs": { + name: {"enabled": True} + for name in ["get_capabilities", "resolve_mapping_plan", "list_jobs", "get_job", "get_qa"] + }, +}] +betas = ["mcp-client-2025-11-20"] +``` + +Include these fields in a Messages request with your chosen supported model, messages and token limit. Authentication is issued separately; BeatForge does not mint provider access tokens. Enable generation/edit/feedback tools only after provisioning the matching scopes. + +Source checked 2026-09-20: [Claude MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector). Neither this API recipe nor the Claude UI has been account-tested here. diff --git a/docs/clients/GROK.md b/docs/clients/GROK.md new file mode 100644 index 0000000..9d12fb9 --- /dev/null +++ b/docs/clients/GROK.md @@ -0,0 +1,21 @@ +# Grok + +Use the custom MCP connector workflow available to your Grok account with the hosted BeatForge `/mcp` endpoint. Never tunnel the privileged local Studio API. The isolated gateway already provides remote HTTP transport. + +For xAI's OpenAI-compatible Responses API, construct this tool server-side and include it in the request to `https://api.x.ai/v1/responses`: + +```python +import os + +beatforge_tool = { + "type": "mcp", + "server_label": "beatforge", + "server_url": os.environ["BEATFORGE_MCP_URL"], + "authorization": os.environ["BEATFORGE_ACCESS_TOKEN"], + "allowed_tools": ["get_capabilities", "resolve_mapping_plan", "list_jobs", "get_job", "get_qa"], +} +``` + +Use your xAI API credential separately for the model request. xAI's current documentation says `require_approval` is unsupported; do not rely on that OpenAI field to protect writes. Use BeatForge scopes and explicit tool allowlists. Add mutation tools only after the user authorizes the workflow. + +Sources checked 2026-09-20: [remote MCP API](https://docs.x.ai/developers/tools/remote-mcp), [custom connectors](https://docs.x.ai/grok/connectors/custom-mcp-tunneling). Grok account setup and inference requests have not been executed here. diff --git a/docs/clients/META_MUSE.md b/docs/clients/META_MUSE.md new file mode 100644 index 0000000..5fdf138 --- /dev/null +++ b/docs/clients/META_MUSE.md @@ -0,0 +1,26 @@ +# Meta Muse / Muse Code + +Muse consumer custom connectors use service API information. Ask Muse to create a BeatForge connector and provide the gateway origin and authenticated `/openapi.json` schema. Supply credentials through its secure credential flow, not chat prose. Request read-only discovery first. Do not assume the consumer app accepts a native MCP URL because Muse Code does. + +Suggested setup description: + +> Connect to my BeatForge gateway. Use its authenticated OpenAPI schema. Start with capabilities, mapping-plan resolution, job history, QA and preview retrieval. Audio uploads use a separate raw-byte endpoint. Generation and revisions return asynchronous job IDs. Record ratings or preferences only when I explicitly provide them. Never report automated QA as a headset playtest. + +Source: [Muse custom connectors](https://www.meta.com/help/artificial-intelligence/1687253048996149/). The exact import and consent flow remains unverified in the user's account. + +For Muse Code, merge this entry into your user settings or project `.mcp.json`: + +```json +{ + "mcpServers": { + "beatforge": { + "type": "streamable-http", + "url": "https://YOUR-GATEWAY/mcp" + } + } +} +``` + +With OAuth configured, use `muse mcp login beatforge --scope read`. Register a client ID with your provider if required. Do not put `${TOKEN}` in HTTP headers: the documented version does not expand it. Plugin MCP entries currently cannot carry credentials, so use authenticated settings instead of claiming a self-contained plugin install. + +Source checked 2026-09-20: [Muse Code MCP settings and OAuth](https://meta-models.github.io/muse-code-sdk/next/guides/extend/mcp-servers/). Configuration readiness is not a successful Muse login. diff --git a/docs/clients/OPENAI.md b/docs/clients/OPENAI.md new file mode 100644 index 0000000..20df517 --- /dev/null +++ b/docs/clients/OPENAI.md @@ -0,0 +1,22 @@ +# GPT / ChatGPT + +For ChatGPT, add the hosted `/mcp` endpoint using the custom app/connector feature available to your account and complete the configured OAuth flow. Account access, OAuth registration and actual tool discovery still need verification; this repository is not a published ChatGPT app. + +For the Responses API, construct the MCP tool server-side: + +```python +import os + +beatforge_tool = { + "type": "mcp", + "server_label": "beatforge", + "server_url": os.environ["BEATFORGE_MCP_URL"], + "authorization": os.environ["BEATFORGE_ACCESS_TOKEN"], + "allowed_tools": ["get_capabilities", "resolve_mapping_plan", "list_jobs", "get_job", "get_qa"], + "require_approval": "always", +} +``` + +Pass this in `tools` with a supported model and your normal Responses request. Handle approval requests explicitly. Supply authorization again on subsequent requests. Add write tools only with the corresponding BeatForge scopes and intended workflow. Never put a token in a prompt or browser bundle. + +Source checked 2026-09-20: [OpenAI MCP guide](https://developers.openai.com/api/docs/guides/tools-connectors-mcp). This configuration has not been run against the OpenAI API or a ChatGPT account. diff --git a/docs/clients/README.md b/docs/clients/README.md new file mode 100644 index 0000000..52728f2 --- /dev/null +++ b/docs/clients/README.md @@ -0,0 +1,21 @@ +# Connect an assistant + +These are setup recipes, not verified account integrations. First provision the isolated gateway using [HOSTING.md](../HOSTING.md), configure [authentication](../CONNECTOR_AUTH.md), and run `tools/check_connector.py`. Use an HTTPS `/mcp` URL reachable by the assistant provider. A cloud assistant cannot reach your PC's localhost address. + +- [Meta Muse and Muse Code](META_MUSE.md) +- [GPT and ChatGPT](OPENAI.md) +- [Claude](CLAUDE.md) +- [Grok](GROK.md) + +The first connection should use only `read` scope. Grant `generate` for uploads and generation, `edit` for revisions/presets/cancellation, and `feedback` for explicit human reports. Never give an assistant a `worker` credential. OAuth tokens must use the configured resource audience. Static pilot tokens must be provisioned into the gateway registry; arbitrary tokens do not create accounts. + +## One verification workflow for every client + +1. Ask for capabilities and resolve “flow, lighter verses, no bombs.” Confirm the returned plan matches Studio. +2. Upload the rights-cleared synthetic sample through the raw authenticated upload endpoint described in CONNECTOR_WORKER.md. Do not put audio bytes or local filesystem paths into MCP arguments. Obtain a ready upload ID. +3. Ask the assistant to generate Hard from that upload with a unique idempotency key. Keep timing confirmation explicit; the synthetic smoke may require `allowUnconfirmed: true`. +4. Inspect the returned job until terminal. Open its preview in Studio; compare job ID, chart hash and QA, then download the ZIP. A queued job with an offline worker is not a generation success. +5. Revise one complete section. Confirm a new child ID, unchanged parent, and full-map QA. Compare both charts at the same song position. Record only feedback the person actually supplies. +6. Test a different account against the job: access must fail. Revoke the test token and confirm access fails according to the configured revocation mechanism. Check duplicate submissions, cancellation, worker reconnect and expired download links. + +Record client/version, date, authentication method, job IDs and results without tokens or signed URLs. Local HTTP MCP verification has passed; these four account-level checks remain pending. API inference fees are separate from Render/Vercel credits. No sample below is executed automatically. diff --git a/pyproject.toml b/pyproject.toml index 0df64d1..d459711 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,9 @@ dependencies = [ [project.optional-dependencies] dev = ["pytest==8.4.2", "playwright==1.55.0"] +studio = ["ortools==9.15.6755", "UnityPy==1.25.3"] +rl = ["torch>=2.6,<3"] +connector = ["mcp==1.30.0", "PyJWT[crypto]>=2.12,<3"] [project.scripts] beatforge = "beatforge.cli:main" diff --git a/render.yaml b/render.yaml index 390e5e2..63387f9 100644 --- a/render.yaml +++ b/render.yaml @@ -1,6 +1,31 @@ services: - type: web - name: beatforge-webmcp + name: beatforge-connector runtime: python - buildCommand: pip install . - startCommand: uvicorn beatforge.api:app --host 0.0.0.0 --port $PORT + plan: 0.5c-512mb + numInstances: 1 + autoDeployTrigger: "off" + buildCommand: pip install -r requirements-gateway.txt + startCommand: PYTHONPATH=src uvicorn beatforge.connector:from_env --factory --host 0.0.0.0 --port $PORT --workers 1 --no-access-log + healthCheckPath: /health + disk: + name: beatforge-data + mountPath: /var/data/beatforge + sizeGB: 1 + envVars: + - key: PYTHON_VERSION + value: 3.12.10 + - key: BEATFORGE_CONNECTOR_DB + value: /var/data/beatforge/jobs.sqlite3 + - key: BEATFORGE_CONNECTOR_HOSTS + sync: false + - key: BEATFORGE_CONNECTOR_ORIGINS + sync: false + - key: BEATFORGE_CONNECTOR_TOKENS + sync: false + - key: BEATFORGE_OAUTH_ISSUER + sync: false + - key: BEATFORGE_OAUTH_JWKS_URL + sync: false + - key: BEATFORGE_OAUTH_RESOURCE + sync: false diff --git a/requirements-gateway.txt b/requirements-gateway.txt new file mode 100644 index 0000000..3fa196c --- /dev/null +++ b/requirements-gateway.txt @@ -0,0 +1,6 @@ +# Gateway only. The local worker installs .[studio,connector] separately. +fastapi>=0.110,<1 +uvicorn[standard]>=0.27,<1 +pydantic>=2.6,<3 +mcp==1.30.0 +PyJWT[crypto]>=2.12,<3 diff --git a/skill-lock.json b/skill-lock.json index 87d0caf..782d810 100644 --- a/skill-lock.json +++ b/skill-lock.json @@ -25,28 +25,41 @@ "scripts/artwork.py": "14125e7137951a3e43303e39c70e802d630bc34b14f8134222075dfdb6a5e941", "scripts/beatforge_core.py": "d42516074d1742af6af78fa13e45ef4b322a483a93b13b5ee3eb60402979acfd", "scripts/bootstrap.py": "8ba79ea3b0ad0ab96a8f6fa562634d6ac6eabd7b43983d19d74cae7ab8ba9dd8", - "scripts/choreography.py": "dfb9d8e9d41e570c86a9e9820dd38d04031d047a8bf51a0657d67dd1a4e99300", + "scripts/choreography.py": "74cb70841ef0b0c8c31aaf7a7833591968a74890350636ee4585cd2dda62a204", "scripts/demucs_stems.py": "3e08440751ae131ad4b9c9f41639b29e98e9e7cc750b54a6c814650e3bff0749", - "scripts/generate_map.py": "bf99db96ea6e0a92c18c745cb426667fb06e39e9bb6cc6a00d738c39518145b8", + "scripts/generate_map.py": "a9fc9052f0b2dc70df01f95fd54f4350cada2e6bf7c2f1dd7162a1fdf7b849ae", "scripts/inspect_map.py": "72675ed6d1a1eda3dc8459c9a86014f6cac1d44fed006385237835c846d88567", "scripts/kinematics.py": "6bfdae6359f335fb143612bfece8509b10c425de852780ffa39077e36a8834ec", + "scripts/mapping_plan.py": "ce42bb69f509d7038244f7b1edae7f20af8f5026d18134fa0f915aa3dd73788d", "scripts/official_corpus.py": "d95c07dd6c613bb483922aa61ce97833bc8c8bf71985df7edbdf03014b847b40", - "scripts/rl/__init__.py": "4794d851da5b10426caa8fc6295788414af9dd5456a19b9cb04c17021c2e172a", + "scripts/rl/README.md": "fb453934ad22b82a1e0a6f2ec2ed3f7de528cbffbb8d7dac9604f60a5d1cb954", + "scripts/rl/__init__.py": "4ca12ccbe2ed1f8f3cc33c9e05a0ba12c80a8680479899b81a2f3eb4385add3d", + "scripts/rl/checkpoints.py": "396d04dbec6cbda7f4e064559bd0cf9cb1d4426994b91dfd387ce2b1d5897c18", "scripts/rl/dataset.py": "674b122175ef8cea9422c22f3f356463c29540ecb7d2644d746f86d2324a6fbc", "scripts/rl/discriminator.py": "ef5fdb1e5080b6620cf7f5a6311024fb7216130d7ebc89b8c029248327b69862", - "scripts/rl/environment.py": "c8878ceffab5e32efddde641b427b0943aef170ffaf08e93ef0e89ba79d14035", - "scripts/rl/models.py": "d72e15d8a6957c630530fc169a8534019bfa22e6158356ac840decb3b4eb440d", - "scripts/rl/policy_generator.py": "9e17acef75a7f58c28ec308bdd33cdd4cef350597224936dab5577e12ce330de", + "scripts/rl/environment.py": "d5769cf08b235183361c12d36d12fc5718d5e7b575621b35e7505c76152275df", + "scripts/rl/evaluate.py": "e4aef75883627c52342a0a7adfc6c636d7474ead9cb524a930d357834f4b6c15", + "scripts/rl/features.py": "36329d02474be69705e596e8a1d9fe4fde2f71e244edf43d41598d6afca8c04b", + "scripts/rl/library.py": "b0ffed20cd1a459596e52404a0e26cda2b2fb9a75c13cce4fe34279a8840f5fc", + "scripts/rl/models.py": "a8fe5a8355a55834b68004630d5911ac9bcd9469a6f38a14a635a89e409a685d", + "scripts/rl/policy_generator.py": "a199e5c47394baa71c5597345f3406ff6cf0ac05c23aa9955df363c0028c96ca", + "scripts/rl/registry.py": "f6b889d8585a8e1e2bdd14d978fe36a36caec614504423946049e40c091e65bd", "scripts/rl/rewards.py": "1ba5d390eebeb3732903520a6525b78724918971491ce71e1995503ee8733117", + "scripts/rl/safety.py": "f4613a31f971eaa5b6fc7d0e73d551053497935f544bc37c616c352c3f001a90", "scripts/rl/train_bc.py": "d9386aabe947532fbee578c97556c5a445215d0752dac38d510c8fb1f6e1d6fe", - "scripts/rl/train_custom_tracks.py": "ec88aa6c41d78795b4bc522ff5ecb8af1bdf081455740ae32b45c2bb439c25f2", - "scripts/rl/train_ppo.py": "fc3c65651c50e9f4300e1a07cdd5d4d17669d5fd6f74309997ea1763aa3a043a", + "scripts/rl/train_custom_tracks.py": "a09708b6cfe47c9147f22e6db5515c5000b0dc00e0681dd2e4f2fb0219c955fa", + "scripts/rl/train_ppo.py": "2506497e125d2f30855947716f3ce2824a6de0f5b0d65ff07a540321e6990de3", "scripts/run_tests.py": "e4f8f872cc82c21d77c9ec97805cb3de7b61bf5042ff15d61b19f8fb18aabdc1", "scripts/safety_contract.py": "ccf1fe75cc2f25c1b6ff72a746b7ed83c1d185985a81f7c560db15c78175cf83", + "scripts/timing_export.py": "9da755603388662f752f575a967603c5254c891e3f42d3bff8be64dc13942fdc", "scripts/validate_map.py": "694f5f205a9b98af627618348a5c1a2c089f317b263e57eb57fb3fbb8cd58c52", "tests/test_kinematics_properties.py": "27c99a739ef75ec9d996575ac85239096d4f109b09a198b1540edcfbbd07432e", "tests/test_pipeline.py": "1823829c10946ff4cc3d48202701d6c9724afecb79b9dab665815d17c944a6f6", - "tests/test_rl_environment.py": "e5c15eeb961eebd1ad58e53eb6c0461b9078865dab5008d22edf4115997527f0", + "tests/test_rl_environment.py": "2ceceebbd63db05b560ab92a9842b8fd119cc2363961d257206378eb70505afc", + "tests/test_rl_features.py": "91d2b171626ec4293e807370f7420cdae96048c05b47a8a47e0f0e5d23047c17", + "tests/test_rl_invariants.py": "5b54f74bdb6b93b49e3e30fb712e8d94853d08a3dcea8eff1ec0295d00f54018", + "tests/test_rl_registry.py": "d7b902cd890a107d74ebb409773ea66c1fe91a906e6e8c2e3da179291b8ad6fb", + "tests/test_rl_training_correctness.py": "2b7c91d772a7ccbb144d8dc92d18955f10ff8ccdc570c44d58c98d40ffc53d70", "tests/test_safety_contract.py": "bdd0a943e7060b068814806250ea51557497b8baf69318dc3ea75730b9c9c5ea" } } diff --git a/skills/beat-saber-mapping/scripts/choreography.py b/skills/beat-saber-mapping/scripts/choreography.py index 9e14a45..0e8de44 100644 --- a/skills/beat-saber-mapping/scripts/choreography.py +++ b/skills/beat-saber-mapping/scripts/choreography.py @@ -25,6 +25,7 @@ ) from kinematics import bomb_on_swing_path, double_is_safe, swing_pose, transition_finding from official_corpus import map_features, normalize_beatmap +from mapping_plan import build_section_plan, normalize_mapping_plan from safety_contract import ( RECOVERY_BEATS, hold_span_beats, @@ -184,6 +185,8 @@ def select_intents(analysis: dict[str, Any], sections: dict[str, Any], difficult ) gap = config.peak_min_gap if is_peak else config.min_gap adaptive_gap = max(gap, gap * (1.15 - 0.35 * intensity)) + density = float(section.get("density", 1.0)) + adaptive_gap = max(gap, adaptive_gap / max(0.35, density)) if beat - last_beat + 1e-9 < adaptive_gap: continue if any( @@ -194,13 +197,16 @@ def select_intents(analysis: dict[str, Any], sections: dict[str, Any], difficult continue strength = float(event.get("strength", 0.0)) energies = event.get("stemEnergy") if isinstance(event.get("stemEnergy"), dict) else {} + dominant = str(section.get("dominantInstrument", "auto")) + if dominant != "auto" and dominant in energies: + strength *= 0.7 + 1.1 * max(0.0, min(1.0, float(energies[dominant]))) layer = str(event.get("layer") or "") instrument = float(energies.get("drums") or 0.0) + float(energies.get("bass") or 0.0) + float(energies.get("guitar") or 0.0) vocal = float(energies.get("vocals") or 0.0) layered = bool(energies) and instrument >= vocal * 1.15 and layer in {"drums", "bass", "guitar", "piano", "other"} if difficulty in {"Expert", "ExpertPlus"} and layered: strength *= 1.35 - threshold = base_threshold + threshold = base_threshold / max(0.35, density) if is_peak: threshold *= 0.88 if difficulty in {"Expert", "ExpertPlus"} else 0.94 elif intensity < 0.4: @@ -256,6 +262,15 @@ def select_intents(analysis: dict[str, Any], sections: dict[str, Any], difficult "layer": layer or "mix", "stemEnergy": energies, } + if "motifId" in section: + relative = beat - float(section.get("startBeat", 0.0)) + motif_offset = int(section.get("motifOffset", 0)) + payload.update({ + "sectionId": section["id"], "motifId": section["motifId"], + "patternBeat": relative + motif_offset * 4.0, + "patternIndex": int(round(relative * 2.0)) + motif_offset, + "style": section.get("style", "balanced"), + }) selected.append(payload) last_beat = beat return selected @@ -433,6 +448,7 @@ def _candidate_notes( index: int, intensity: float, beat: float, + preference_beat: float | None = None, ) -> list[dict[str, Any]]: """Enumerate combo-first arrow poses. Dots are a rare reset, not the default cut.""" @@ -440,8 +456,9 @@ def _candidate_notes( home = (0, 1) else: home = (3, 2) - preferred = swing_family(color, parity, beat) - lead = cut_for(color, parity, index, beat) + phase = beat if preference_beat is None else preference_beat + preferred = swing_family(color, parity, phase) + lead = cut_for(color, parity, index, phase) notes: list[dict[str, Any]] = [] seen: set[tuple[int, int, int]] = set() @@ -627,9 +644,14 @@ def _pose_domain( intensity = float(intent.get("intensity", 0.5)) kind = intent["kind"] duration = float(intent.get("duration") or 0.0) - heads = _candidate_notes(color, parity, index, intensity, beat) + index = int(intent.get("patternIndex", index)) + int(intent.get("candidateVariant", 0)) + phase = float(intent.get("patternBeat", beat)) + heads = _candidate_notes(color, parity, index, intensity, beat, preference_beat=phase) + if intent.get("style") in {"flow", "chill"}: + home_x = 0.5 if color == 0 else 2.5 + heads.sort(key=lambda head: abs(float(head["x"]) - home_x) + (0.5 if int(head["d"]) in {2, 3, 8} else 0.0)) if str(intent.get("layer") or "") == "combo-stack": - lead = cut_for(color, parity, index, beat) + lead = cut_for(color, parity, index, phase) if color == 0: pair_cells = ((1, 1), (0, 1), (1, 0), (1, 2)) else: @@ -667,7 +689,9 @@ def _joint_combined_domain( beat = float(intent["beat"]) stack = str(intent.get("layer") or "") == "combo-stack" - lead_parity = int(beat // 4.0) % 2 if stack else (index + int(beat // 2.0)) % 2 + pattern_beat = float(intent.get("patternBeat", beat)) + pattern_index = int(intent.get("patternIndex", index)) + int(intent.get("candidateVariant", 0)) + lead_parity = int(pattern_beat // 4.0) % 2 if stack else (pattern_index + int(pattern_beat // 2.0)) % 2 buckets = { (color_id, parity): _pose_domain(intent, color_id, parity, index, bpm, recovery_beats)[:6] for color_id in (0, 1) @@ -2500,12 +2524,13 @@ def score_candidate(database_path: Path | None, features: dict[str, Any]) -> dic } -def generate_all( +def _generate_single_pass( analysis: dict[str, Any], sections: dict[str, Any], seed: int, corpus_database: Path | None = None, difficulties: Sequence[str] | None = None, + candidate_variant: int = 0, ) -> tuple[dict[str, dict[str, Any]], dict[str, Any]]: generated: dict[str, dict[str, Any]] = {} report: dict[str, Any] = {"solver": {}, "difficulties": {}, "officialReferences": {}} @@ -2513,6 +2538,8 @@ def generate_all( for difficulty in chosen: emit_progress("choreography", f"selecting {difficulty} intents from onsets and sections") intents = select_intents(analysis, sections, difficulty) + if candidate_variant: + intents = [{**intent, "candidateVariant": candidate_variant} for intent in intents] joint = None try: emit_progress("choreography", f"solving joint CP-SAT for {difficulty} ({len(intents)} events)") @@ -2559,3 +2586,97 @@ def generate_all( ) report["difficulties"][difficulty]["corpusRank"] = score_candidate(corpus_database, candidate_features) return generated, report + + +def rank_choreography_candidate( + beatmap: dict[str, Any], difficulty: str, bpm: float, + corpus_rank: dict[str, Any], reference: dict[str, Any], + expected_count: int, +) -> dict[str, Any]: + """Score complete candidates only after running the unchanged structural gates.""" + from beatforge_core import ValidationReport + from validate_map import validate_v3 + + validation = ValidationReport() + validate_v3(beatmap, f"{difficulty}Standard.dat", validation, bpm=bpm, difficulty=difficulty) + notes = beatmap.get("colorNotes", []) + errors = [issue.code for issue in validation.errors] + if expected_count and not notes: + errors.append("NO_PLAYABLE_NOTES") + events, _ = normalize_beatmap(beatmap) + features = map_features(events, bpm) + profile = reference.get("profile", {}) + comparable = [name for name in ("p95Reach", "centerVisionRate", "medianSameHandGap") if name in profile] + profile_distance = sum(abs(float(features.get(name, 0.0)) - float(profile[name])) / max(0.25, abs(float(profile[name]))) for name in comparable) / max(1, len(comparable)) + reference_score = 1.0 / (1.0 + profile_distance) if comparable else 0.5 + corpus_score = float(corpus_rank.get("score", 50.0)) / 100.0 + coverage = min(1.0, len(notes) / max(1, expected_count)) + signatures = [(int(note["x"]), int(note["y"]), int(note["d"])) for note in notes] + repeats = sum(left == right for left, right in zip(signatures, signatures[1:])) + variety = 1.0 - repeats / max(1, len(signatures) - 1) + score = 100.0 * (0.4 * corpus_score + 0.25 * reference_score + 0.25 * coverage + 0.1 * variety) + return { + "score": round(score, 6), "hardErrors": sorted(set(errors)), + "eligible": not errors, "corpusScore": round(corpus_score * 100.0, 6), + "referenceScore": round(reference_score * 100.0, 6), + "eventCoverage": round(coverage, 6), "variety": round(variety, 6), + "features": features, + } + + +def select_ranked_candidate(candidates: list[dict[str, Any]]) -> dict[str, Any]: + eligible = [candidate for candidate in candidates if candidate.get("eligible")] + if not eligible: + codes = sorted({code for candidate in candidates for code in candidate.get("hardErrors", [])}) + raise ValueError("No candidate passed the unchanged safety gates: " + ", ".join(codes)) + return max(eligible, key=lambda candidate: (candidate["score"], -candidate["index"])) + + +def generate_all( + analysis: dict[str, Any], sections: dict[str, Any], seed: int, + corpus_database: Path | None = None, + difficulties: Sequence[str] | None = None, + mapping_plan: dict[str, Any] | None = None, +) -> tuple[dict[str, dict[str, Any]], dict[str, Any]]: + if mapping_plan is None: + return _generate_single_pass(analysis, sections, seed, corpus_database, difficulties) + controls = normalize_mapping_plan(mapping_plan) + planned = build_section_plan(analysis, sections, controls) + chosen = tuple(name for name in DIFFICULTIES if name in set(difficulties or DIFFICULTIES)) or DIFFICULTIES + generated: dict[str, dict[str, Any]] = {} + report: dict[str, Any] = {"solver": {}, "difficulties": {}, "officialReferences": {}, "candidateRanking": {}, "mappingPlan": planned} + for difficulty in chosen: + # Retrieve before generation so every candidate is compared with the same reference family. + reference = retrieve_official_references(corpus_database, difficulty, float(analysis["bpm"])) + intents = select_intents(analysis, planned, difficulty) + candidates = [] + payloads: dict[int, tuple[dict[str, Any], dict[str, Any]]] = {} + for index in range(controls["candidateCount"]): + emit_progress("choreography", f"{difficulty}: generating candidate {index + 1}/{controls['candidateCount']}") + try: + maps, trace = _generate_single_pass(analysis, planned, seed + index * 7919, corpus_database, (difficulty,), candidate_variant=index) + beatmap = maps[difficulty] + if controls["noBombs"]: + beatmap["bombNotes"] = [] + if controls["noWalls"]: + beatmap["obstacles"] = [] + ranked_map = beatmap + if len(analysis.get("beatGrid", [])) >= 2: + from timing_export import project_map_timing + ranked_map = project_map_timing(beatmap, analysis) + ranked_events, _ = normalize_beatmap(ranked_map) + corpus_rank = score_candidate(corpus_database, map_features(ranked_events, float(analysis["bpm"]))) + rank = rank_choreography_candidate(ranked_map, difficulty, float(analysis["bpm"]), corpus_rank, reference, len(intents)) + trace["difficulties"][difficulty]["corpusRank"] = corpus_rank + candidates.append({"index": index, **rank}) + payloads[index] = (beatmap, trace) + except (RuntimeError, ValueError) as error: + candidates.append({"index": index, "eligible": False, "score": 0.0, "hardErrors": [str(error)]}) + winner = select_ranked_candidate(candidates) + beatmap, trace = payloads[winner["index"]] + generated[difficulty] = beatmap + report["solver"][difficulty] = trace["solver"][difficulty] + report["difficulties"][difficulty] = trace["difficulties"][difficulty] + report["officialReferences"][difficulty] = reference + report["candidateRanking"][difficulty] = {"selectedIndex": winner["index"], "candidates": candidates} + return generated, report diff --git a/skills/beat-saber-mapping/scripts/generate_map.py b/skills/beat-saber-mapping/scripts/generate_map.py index 81d79be..f189144 100644 --- a/skills/beat-saber-mapping/scripts/generate_map.py +++ b/skills/beat-saber-mapping/scripts/generate_map.py @@ -17,6 +17,8 @@ from beatforge_core import DIFFICULTIES, TOOL_VERSION, default_cache_dir, difficulty_rank, emit_progress, encode_ogg, find_ffmpeg, load_json, parse_difficulties, sha256_file, write_json from choreography import CONFIGS, generate_all +from mapping_plan import build_section_plan, normalize_mapping_plan +from timing_export import project_map_timing, project_plan_timing from artwork import derive_palette, extract_embedded_artwork, lookup_itunes_cover, lookup_release_cover, palette_from_rgb, write_palette_cover from official_corpus import corpus_is_fresh, detect_game_root, sync as sync_official_corpus @@ -373,6 +375,7 @@ def main() -> int: parser.add_argument("--profile", default="official-premium", choices=["official-premium", "official-rl"]) parser.add_argument("--engine", default="cp-sat", choices=["cp-sat", "rl", "hybrid"], help="Choreography engine to use (cp-sat, rl, hybrid)") parser.add_argument("--rl-model", type=Path, help="Path to trained PyTorch RL policy checkpoint") + parser.add_argument("--mapping-plan", type=Path, help="Version 1 structured creative controls JSON") parser.add_argument("--environment", default=None, help="Beat Saber environment name (e.g. DaftPunkEnvironment, DefaultEnvironment)") parser.add_argument("--full-spread", action="store_true", default=True) parser.add_argument( @@ -393,6 +396,13 @@ def main() -> int: parser.add_argument("--game-root", type=Path, help="Beat Saber install root; auto-detected on common Oculus/Steam paths") parser.add_argument("--seed", type=int) args = parser.parse_args() + try: + creative_controls = normalize_mapping_plan(load_json(args.mapping_plan)) if args.mapping_plan else None + if (args.engine == "rl" or args.profile == "official-rl") and (args.rl_model is None or not args.rl_model.is_file()): + raise ValueError("The RL engine requires an existing trained checkpoint supplied with --rl-model") + except (ValueError, OSError) as error: + print(f"ERROR: {error}", file=sys.stderr) + return 2 if not args.audio.is_file(): print(f"ERROR: audio file does not exist: {args.audio}", file=sys.stderr) return 2 @@ -423,6 +433,7 @@ def main() -> int: emit_progress("timing", "reusing existing analysis.json") analysis = load_json(analysis_file) sections = load_json(sections_file) + creative_plan = build_section_plan(analysis, sections, creative_controls) if creative_controls is not None else None unconfirmed = bool(args.continue_unconfirmed) if analysis.get("status") != "timing_verified": if not unconfirmed: @@ -496,54 +507,33 @@ def main() -> int: emit_progress("choreography", f"solving {', '.join(chosen)}") if args.engine == "rl" or args.profile == "official-rl": from rl.policy_generator import RLMapGenerator + from rl.features import load_analysis_features rl_gen = RLMapGenerator(model_path=args.rl_model) - raw_bg = load_json(analysis_dir / "beat_grid.json") if (analysis_dir / "beat_grid.json").exists() else None - if isinstance(raw_bg, dict) and "tempoRegions" in raw_bg: - max_beat = max((float(r.get("endBeat", 0.0)) for r in raw_bg.get("tempoRegions", [])), default=0.0) - beat_grid = [round(i * 0.25, 4) for i in range(max(16, int(max_beat * 4)))] - elif isinstance(raw_bg, list): - beat_grid = [float(b) for b in raw_bg] - else: - duration_s = float(analysis.get("durationSeconds", analysis.get("duration", 240.0))) - bpm_val = float(analysis.get("bpm", 125.0)) - beat_grid = [round(i * 0.25, 4) for i in range(max(16, int(duration_s * bpm_val / 60.0 * 4)))] - - grid_len = len(beat_grid) - try: - import numpy as np - from beatforge_core import frame_features, load_audio - audio_buf = load_audio(args.audio) - feat = frame_features(audio_buf.samples) - flux = feat["flux"] - onsets = np.interp(np.linspace(0, len(flux), grid_len), np.arange(len(flux)), flux) - onsets = ((onsets - np.min(onsets)) / (np.max(onsets) - np.min(onsets) + 1e-6)).tolist() - except Exception: - onsets = [0.5] * grid_len - - audio_features = { - "onsets": onsets, - "flux": onsets, - "stems": { - "drums": onsets, - "bass": onsets, - "vocals": [o * 0.7 for o in onsets], - "guitar": [o * 0.5 for o in onsets], - "piano": [o * 0.5 for o in onsets], - "other": [o * 0.4 for o in onsets], - }, - "sections": [s.get("type", "verse") for s in sections] if isinstance(sections, list) else ["verse"] * grid_len, - } + bundle = load_analysis_features(analysis_dir, require_verified=not unconfirmed) maps = {} for diff in chosen: - maps[diff] = rl_gen.generate_difficulty(audio_features, beat_grid, float(analysis["bpm"]), difficulty=diff) + maps[diff] = rl_gen.generate_difficulty(bundle.audio_features, bundle.beat_grid, bundle.bpm, difficulty=diff, seed=seed) + if creative_controls and creative_controls["noBombs"]: + maps[diff]["bombNotes"] = [] + if creative_controls and creative_controls["noWalls"]: + maps[diff]["obstacles"] = [] choreography_report = { "poseSolver": "rl-policy-v1", "solver": "rl-policy-v1", "difficulties": chosen, + "checkpoint": rl_gen.checkpoint_metadata, + "features": bundle.provenance, + "mappingPlan": creative_plan, + "creativeControlSupport": "RL applies noBombs and noWalls only; phrase, density, and style controls require the premium engine", } else: - maps, choreography_report = generate_all(analysis, sections, seed, args.corpus_database, difficulties=chosen) + maps, choreography_report = generate_all(analysis, sections, seed, args.corpus_database, difficulties=chosen, mapping_plan=creative_controls) + maps = {difficulty: project_map_timing(payload, analysis) for difficulty, payload in maps.items()} + if creative_plan is not None: + creative_plan = project_plan_timing(creative_plan, analysis) + choreography_report["mappingPlan"] = creative_plan + write_json(package_analysis_dir / "mapping_plan.json", creative_plan) for difficulty, payload in maps.items(): write_json(args.out / f"{difficulty}Standard.dat", payload) emit_progress("audio", "encoding song.ogg for the Beat Saber pack") @@ -590,6 +580,7 @@ def main() -> int: }, }, "choreography": choreography_report, + "mappingPlan": creative_plan, "officialCorpus": {"database": str(args.corpus_database.resolve()), **corpus_report}, "documentation": { "manifest": "_beatforge/documentation_manifest.json", diff --git a/skills/beat-saber-mapping/scripts/mapping_plan.py b/skills/beat-saber-mapping/scripts/mapping_plan.py new file mode 100644 index 0000000..7e4a0bb --- /dev/null +++ b/skills/beat-saber-mapping/scripts/mapping_plan.py @@ -0,0 +1,133 @@ +"""Versioned, deterministic creative controls shared by Studio and the portable mapper.""" + +from __future__ import annotations + +import math +import re +from typing import Any + +SCHEMA_VERSION = 1 +INSTRUMENTS = ("auto", "drums", "bass", "vocals", "guitar", "piano", "other") +STYLES = ("balanced", "flow", "tech", "chill") +SCALE_FIELDS = ("intensity", "density", "verseDensity", "chorusDensity") +DEFAULTS = { + "schemaVersion": SCHEMA_VERSION, "brief": "", "intensity": 1.0, + "density": 1.0, "verseDensity": 1.0, "chorusDensity": 1.0, + "dominantInstrument": "auto", "style": "balanced", "noBombs": False, + "noWalls": False, "candidateCount": 2, "sectionOverrides": [], +} + + +def _scale(value: Any, field: str) -> float: + if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or not 0.5 <= value <= 1.5: + raise ValueError(f"{field} must be a number from 0.5 to 1.5") + return float(value) + + +def normalize_mapping_plan(value: dict[str, Any] | None = None) -> dict[str, Any]: + """Resolve supported brief phrases, with explicit controls taking precedence.""" + if value is None: + value = {} + if not isinstance(value, dict): + raise ValueError("mapping plan must be an object") + unknown = set(value) - set(DEFAULTS) - {"interpretation"} + if unknown: + raise ValueError("unknown mapping plan controls: " + ", ".join(sorted(unknown))) + if type(value.get("schemaVersion", 1)) is not int or value.get("schemaVersion", 1) != 1: + raise ValueError("mapping plan schemaVersion must be 1") + brief = value.get("brief", "") + if not isinstance(brief, str) or len(brief) > 2000: + raise ValueError("brief must be text of at most 2000 characters") + text = brief.casefold() + inferred: dict[str, Any] = {} + recognized: list[str] = [] + phrases = [ + (r"\b(?:no|without) bombs\b", "noBombs", True, "No bombs"), + (r"\b(?:no|without) walls\b", "noWalls", True, "No walls"), + (r"\b(?:sparse|lighter|easier) verses?\b", "verseDensity", 0.75, "Lighter verses"), + (r"\b(?:dense|denser|bigger|stronger) chorus(?:es)?\b", "chorusDensity", 1.25, "Denser choruses"), + (r"\b(?:less|fewer) notes\b", "density", 0.75, "Fewer notes"), + (r"\bmore notes\b", "density", 1.25, "More notes"), + (r"\b(?:relaxed|chill)\b", "style", "chill", "Chill style"), + (r"\b(?:flow|flowing|smooth)\b", "style", "flow", "Flow style"), + (r"\b(?:tech|technical)\b", "style", "tech", "Tech style"), + ] + for pattern, field, setting, description in phrases: + if re.search(pattern, text): + inferred[field] = setting + if field not in value: + recognized.append(description) + for instrument in INSTRUMENTS[1:]: + if re.search(r"\b(?:follow|focus on|emphasize) (?:the )?" + instrument + r"\b", text): + inferred["dominantInstrument"] = instrument + if "dominantInstrument" not in value: + recognized.append("Follow " + instrument) + result = {**DEFAULTS, **inferred, **value, "brief": brief} + for field in SCALE_FIELDS: + result[field] = _scale(result[field], field) + if result["dominantInstrument"] not in INSTRUMENTS: + raise ValueError("unknown dominantInstrument") + if result["style"] not in STYLES: + raise ValueError("unknown mapping style") + for field in ("noBombs", "noWalls"): + if not isinstance(result[field], bool): + raise ValueError(f"{field} must be a boolean") + count = result["candidateCount"] + if isinstance(count, bool) or not isinstance(count, int) or not 1 <= count <= 3: + raise ValueError("candidateCount must be an integer from 1 to 3") + overrides = result["sectionOverrides"] + if not isinstance(overrides, list) or len(overrides) > 200: + raise ValueError("sectionOverrides must be a list with at most 200 entries") + checked = [] + seen = set() + for override in overrides: + if not isinstance(override, dict) or set(override) - {"id", "density", "intensity", "dominantInstrument"}: + raise ValueError("invalid section override") + section_id = override.get("id") + if not isinstance(section_id, str) or not section_id or len(section_id) > 100 or section_id in seen: + raise ValueError("section overrides need unique nonempty ids") + seen.add(section_id) + item = dict(override) + for field in ("density", "intensity"): + if field in item: + item[field] = _scale(item[field], "section " + field) + if "dominantInstrument" in item and item["dominantInstrument"] not in INSTRUMENTS: + raise ValueError("unknown section dominantInstrument") + checked.append(item) + result["sectionOverrides"] = checked + interpretation = value.get("interpretation", []) + if not isinstance(interpretation, list) or not all(isinstance(item, str) for item in interpretation): + raise ValueError("interpretation must be a list of phrases") + result["interpretation"] = recognized or list(interpretation) + return result + + +def build_section_plan(analysis: dict[str, Any], sections: dict[str, Any], value: dict[str, Any]) -> dict[str, Any]: + plan = normalize_mapping_plan(value) + source = sections.get("sections") or [{"label": "body", "startBeat": 0.0, "endBeat": float(analysis.get("durationSeconds", 0.0)) * float(analysis.get("bpm", 120.0)) / 60.0, "intensity": 0.5}] + overrides = {entry["id"]: entry for entry in plan["sectionOverrides"]} + style_scale = {"balanced": 1.0, "flow": 0.95, "tech": 1.05, "chill": 0.75}[plan["style"]] + planned = [] + for index, section in enumerate(source): + item = dict(section) + section_id = str(item.get("id") or f"section-{index:03d}") + label = str(item.get("label") or item.get("type") or "body") + family = re.sub(r"[\d\W_]+", "", label.casefold()) or "body" + chorus = any(word in family for word in ("chorus", "drop", "peak")) + verse = "verse" in family + factor = plan["chorusDensity"] if chorus else plan["verseDensity"] if verse else 1.0 + override = overrides.get(section_id, {}) + density = plan["density"] * factor * style_scale * override.get("density", 1.0) + item.update({ + "id": section_id, "label": label, "motifId": family, + "motifOffset": sum((i + 1) * ord(character) for i, character in enumerate(family)) % 4, + "density": round(max(0.35, min(1.75, density)), 6), + "intensity": max(0.0, min(1.0, float(item.get("intensity", 0.5)) * plan["intensity"] * override.get("intensity", 1.0))), + "dominantInstrument": override.get("dominantInstrument", plan["dominantInstrument"]), + "style": plan["style"], + }) + planned.append(item) + missing = set(overrides) - {item["id"] for item in planned} + if missing: + raise ValueError("unknown section ids: " + ", ".join(sorted(missing))) + return {"schemaVersion": 1, "source": sections.get("source", "internal"), "sections": planned, "controls": plan} diff --git a/skills/beat-saber-mapping/scripts/rl/README.md b/skills/beat-saber-mapping/scripts/rl/README.md new file mode 100644 index 0000000..2b121cf --- /dev/null +++ b/skills/beat-saber-mapping/scripts/rl/README.md @@ -0,0 +1,148 @@ +# RL training, evaluation, and checkpoint promotion + +RL remains experimental. The studio's constraint solver is the default. A saved +PPO reward is not proof of musical quality, and generated maps still require the +normal package validator and human headset checks. + +Set `PYTHONPATH` to `skills/beat-saber-mapping/scripts` and use the project's Python +environment. PyTorch is an optional dependency. These commands operate on local +files and do not download models, analyze new audio, or install songs. + +## Prepare a song library + +Run `analyze_audio.py` and resolve the normal timing gate first. Every song needs +`analysis.json`, `beat_grid.json`, `sections.json`, and `audio_features.npz`. +Training and evaluation require `timing_verified`. The studio's explicit +unconfirmed-generation override is the only caller that disables this check, +and its feature provenance continues to say `needs_anchors`. + +Use a manifest outside the repository for your private audio library: + +```json +{ + "schemaVersion": 1, + "tracks": [ + {"id": "training-song", "split": "train", "analysisDir": "analysis/training-song"}, + {"id": "validation-song", "split": "validation", "analysisDir": "analysis/validation-song"}, + {"id": "final-test-song", "split": "test", "analysisDir": "analysis/final-test-song"} + ] +} +``` + +Paths resolve relative to the manifest. IDs and analyzed audio SHA-256 hashes +must be unique across the entire library. Use varied songs and keep the final +test split untouched while selecting models. Audio, analysis, maps, checkpoints, +and evaluation outputs stay out of version control. + +## Train with the same inputs used at generation + +```text +python -m rl.train_custom_tracks --manifest library.json --rounds 1 --timesteps-per-track 16384 --seed 42 --out data/models/candidate.pt +python -m rl.train_ppo --analysis-dir analysis/song --timesteps 16384 --seed 42 --out data/models/single-song.pt +``` + +`--analysis-dir` can be repeated for library training without a manifest. +`--no-resume` explicitly starts fresh. Resuming a real-song run from a checkpoint +without recorded training hashes is rejected because held-out independence +cannot be established. Old `--ram-dir`, `--ninajirachi-dir`, and `--spotify-dir` +discovery was replaced by explicit artifact directories or a manifest. + +The training loop uses the requested number of transitions exactly. Full song +duration stays in the environment; a short requested training budget may still +cover only part of a song. The default is one library round. It no longer silently +performs 99 rounds. Seeds cover initialization, policy sampling, and batch order. +Repeatability applies to the same device and runtime versions. + +The second hand's action mask uses the first hand's selected note. Simultaneous +handclaps, intersecting saber paths, and center vision doubles are masked during +PPO collection and inference. PPO stores that conditional mask for replay, so +the action probability used during optimization matches collection. This mask +does not replace validation of holds, walls, bombs, and the final package. + +For a software smoke test, use the explicit `--synthetic` option on `train_ppo`. +That pulse has no separated stems or verified song timing and cannot establish +held-out song performance. + +`rl.features.load_analysis_features(path)` returns `AnalysisFeatures` with +`audio_features`, `beat_grid`, `bpm`, `provenance`, and the original `analysis`. +The schema is `beatforge-rl-sample-grid-v2`. Quarter-beat decision samples come +from the monotonic integer sample grid, including offsets and tempo changes. +Frame interpolation uses saved sample positions, seconds, or `hopSamples`. +`sections.json.sections` supplies interval labels. Sample and second boundaries +take precedence over derived beat fields. Real Demucs event `stemEnergy` +annotations enter their nearest decision step. Missing stems remain zero and +`stemAvailability` records which stems actually exist. Missing spectral flux is +zero with `fluxAvailable: false`; the mix onset envelope is never relabeled as a +separated instrument. The 69-value policy observation layout remains compatible; +availability flags and provenance are metadata, while section labels influence +the musical reward. + +Checkpoints contain `state_dict` and JSON-compatible `metadata`, including the +feature schema, seed, training audio hashes, and analysis provenance. Saves use +an atomic replacement. Inference uses `weights_only=True`, checks tensor shapes +and finite values, and records the exact checkpoint SHA-256. Legacy raw state +dicts can still be loaded explicitly for generation with +`featureSchema: legacy-unrecorded`. They cannot enter a verified held-out +comparison. A missing or incompatible checkpoint never falls back to a random +policy. Callers supplying a policy instance directly are marked +`source: supplied-policy`, for tests or explicit application integration. + +## Compare on held-out songs + +```text +python -m rl.evaluate --manifest library.json --checkpoint data/models/baseline.pt --checkpoint data/models/candidate.pt --split validation --difficulty Expert --seed 42 --out data/evaluations/comparison.json +``` + +The command checks checkpoint training hashes against held-out audio hashes, +generates each chart deterministically, projects it onto the exact audio clock, +and runs the shared chart validator. It reports hard errors, warning codes, +NPS, hand imbalance, repeated transitions, section density, and the mean onset +strength at note positions. Empty charts count as failures. The report includes +sample artifact hashes, checkpoint hashes, evaluator code hashes, runtime +versions, chart hashes, and the seed. Exit status 2 means a generated chart has +hard errors; the JSON report is still saved. + +These measurements do not replace packaged-map checks or human evaluation. +Blind preferences, manual corrections, and headset playtests are separate +evidence. No generated report claims those checks happened. + +## Promote and roll back + +```text +python -m rl.registry --registry data/model_registry initialize --checkpoint data/models/baseline.pt --evaluation data/evaluations/comparison.json +python -m rl.registry --registry data/model_registry promote --checkpoint data/models/candidate.pt --evaluation data/evaluations/comparison.json +python -m rl.registry --registry data/model_registry active +python -m rl.registry --registry data/model_registry rollback +``` + +Initialization explicitly selects a validated, nonempty baseline. Promotion +requires identical manifest, split, song analysis hashes, difficulty, seed, +evaluator code, feature schema, and runtime versions. Each candidate chart must +have zero hard errors and no increase in warnings, hand imbalance, repeated +transition fraction, or distance from the difficulty's target NPS. No held-out +song may regress in onset alignment, and the mean onset-strength gain must be +at least 0.001. This is a measured candidate improvement, not proof that humans +prefer it. If the benchmark or evaluator changes, evaluate both models again +and explicitly establish a new registry baseline. + +The registry copies checkpoints and evaluations into folders named by their +content hashes. `registry.json` contains `schemaVersion`, `active`, `models`, +and an append-only `history` of selections. Promotion atomically switches the +active reference and retains prior files for rollback. It does not delete or +overwrite the source checkpoints. Concurrent writes fail with a lock error. +`active_checkpoint(registry_root)` verifies the active file's hash before +returning its path. A feedback-triggered local job can call +`register_checkpoint(root, checkpoint, evaluation)` after an explicitly run +training/evaluation cycle; this module starts no background work. + +## Focused checks + +```text +python -m pytest skills/beat-saber-mapping/tests/test_rl_features.py skills/beat-saber-mapping/tests/test_rl_training_correctness.py skills/beat-saber-mapping/tests/test_rl_registry.py skills/beat-saber-mapping/tests/test_rl_environment.py +``` + +Tests cover offset and tempo-change sampling, localized real stems, missing +inputs, section intervals, train/eval content leakage, checkpoint corruption, +terminal PPO bootstrap boundaries, exact budgets, seeds, promotion rejection, +and artifact-preserving rollback. They use small synthetic fixtures rather than +training on or redistributing private song audio. diff --git a/skills/beat-saber-mapping/scripts/rl/__init__.py b/skills/beat-saber-mapping/scripts/rl/__init__.py index b1b74ac..5a22b08 100644 --- a/skills/beat-saber-mapping/scripts/rl/__init__.py +++ b/skills/beat-saber-mapping/scripts/rl/__init__.py @@ -2,9 +2,18 @@ from __future__ import annotations -from .environment import BeatSaberEnv -from .models import ActorCriticPolicy, BeatForgeAudioEncoder -from .rewards import CompositeReward, KinematicReward, MusicalReward, StyleReward +def __getattr__(name): + # Feature extraction and benchmark metric helpers work without PyTorch. + if name in {"ActorCriticPolicy", "BeatForgeAudioEncoder"}: + from . import models + return getattr(models, name) + if name == "BeatSaberEnv": + from .environment import BeatSaberEnv + return BeatSaberEnv + if name in {"CompositeReward", "KinematicReward", "MusicalReward", "StyleReward"}: + from . import rewards + return getattr(rewards, name) + raise AttributeError(name) __all__ = [ "BeatSaberEnv", diff --git a/skills/beat-saber-mapping/scripts/rl/checkpoints.py b/skills/beat-saber-mapping/scripts/rl/checkpoints.py new file mode 100644 index 0000000..bb3fe62 --- /dev/null +++ b/skills/beat-saber-mapping/scripts/rl/checkpoints.py @@ -0,0 +1,58 @@ +"""Validated policy weights and portable, non-pickle provenance metadata.""" + +from __future__ import annotations + +from pathlib import Path +import json +from typing import Any + +import torch + +from .features import FEATURE_SCHEMA, file_sha256 +from .models import ActorCriticPolicy + + +def load_checkpoint(path: Path, device: str = "cpu") -> tuple[ActorCriticPolicy, dict[str, Any]]: + path = Path(path) + if not path.is_file(): + raise FileNotFoundError(f"RL checkpoint does not exist: {path}") + try: + payload = torch.load(path, map_location=device, weights_only=True) + if not isinstance(payload, dict): + raise ValueError("checkpoint must contain policy weights") + wrapped = "state_dict" in payload + weights = payload["state_dict"] if wrapped else payload + metadata = dict(payload.get("metadata", {})) if wrapped else {"featureSchema": "legacy-unrecorded"} + json.dumps(metadata, allow_nan=False) + for name in ("trainingSourceSha256", "heldOutSourceSha256"): + if name in metadata: + hashes = metadata[name] + if (not isinstance(hashes, list) or not all(isinstance(value, str) and len(value) == 64 + and all(character in "0123456789abcdef" for character in value) for value in hashes)): + raise ValueError(f"Invalid checkpoint lineage field: {name}") + schema = metadata.get("featureSchema", "legacy-unrecorded") + if schema not in {FEATURE_SCHEMA, "legacy-unrecorded"}: + raise ValueError(f"unsupported feature schema: {schema}") + first = weights["encoder.0.weight"] + if first.ndim != 2 or first.shape[1] != 69: + raise ValueError("checkpoint observation dimensions are incompatible with the RL environment") + with torch.random.fork_rng(devices=[]): + policy = ActorCriticPolicy(obs_dim=int(first.shape[1]), hidden_dim=int(first.shape[0])) + if not all(isinstance(value, torch.Tensor) and torch.is_floating_point(value) and torch.isfinite(value).all() for value in weights.values()): + raise ValueError("checkpoint contains non-finite or invalid weights") + policy.load_state_dict(weights, strict=True) + except Exception as exc: + raise ValueError(f"Invalid RL checkpoint {path.name}: {exc}") from exc + policy.to(device) + policy.eval() + metadata.update({"checkpointFile": path.name, "checkpointSha256": file_sha256(path)}) + return policy, metadata + + +def save_checkpoint(policy: ActorCriticPolicy, path: Path, metadata: dict[str, Any]) -> None: + """Replace checkpoints atomically so interruption cannot destroy a prior save.""" + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(path.name + ".tmp") + torch.save({"state_dict": policy.state_dict(), "metadata": {**metadata, "featureSchema": FEATURE_SCHEMA}}, temporary) + temporary.replace(path) diff --git a/skills/beat-saber-mapping/scripts/rl/environment.py b/skills/beat-saber-mapping/scripts/rl/environment.py index db1d13f..4870237 100644 --- a/skills/beat-saber-mapping/scripts/rl/environment.py +++ b/skills/beat-saber-mapping/scripts/rl/environment.py @@ -21,6 +21,7 @@ vision_blocking_double, ) from safety_contract import RECOVERY_BEATS, occupied_until +from .safety import ValidationClock, transition_is_safe try: from .rewards import CompositeReward, RewardBreakdown except ImportError: @@ -71,10 +72,14 @@ def __init__( difficulty: str = "Expert", lookahead_steps: int = 4, ): - self.bpm = max(1.0, float(bpm)) + self.bpm = float(bpm) + if not math.isfinite(self.bpm) or self.bpm <= 0: + raise ValueError("RL BPM must be finite and positive") + self.base_bpm = self.bpm self.difficulty = difficulty if difficulty in DIFFICULTIES else "Expert" self.target_nps = TARGET_NPS_MAP.get(self.difficulty, 4.25) self.lookahead_steps = max(1, lookahead_steps) + self.rng = np.random.default_rng(0) self.reward_engine = CompositeReward( difficulty=self.difficulty, @@ -84,9 +89,10 @@ def __init__( # Internal state buffers self.beat_grid = self._normalize_beat_grid(beat_grid) if beat_grid is not None else self._generate_synthetic_grid(64, self.bpm) - self.audio_features = audio_features or self._generate_synthetic_audio(len(self.beat_grid)) + self.audio_features = audio_features if audio_features is not None else self._generate_synthetic_audio(len(self.beat_grid)) self.current_step = 0 self.total_steps = len(self.beat_grid) + self.validation_clock = ValidationClock(self.audio_features, self.beat_grid, self.base_bpm) # Kinematic hand states self.red_prev: Optional[Dict[str, Any]] = None @@ -106,31 +112,25 @@ def _normalize_beat_grid(self, beat_grid: Any) -> List[float]: if beat_grid is None: return self._generate_synthetic_grid(64, self.bpm) if isinstance(beat_grid, dict): - if "beats" in beat_grid and isinstance(beat_grid["beats"], list): - return [float(b) for b in beat_grid["beats"]] - if "tempoRegions" in beat_grid: - max_beat = max((float(r.get("endBeat", 0.0)) for r in beat_grid.get("tempoRegions", [])), default=64.0) - return [round(i * 0.25, 4) for i in range(max(16, int(max_beat * 4)))] - return self._generate_synthetic_grid(64, self.bpm) - return [float(b) for b in beat_grid] + raise ValueError("Use rl.features.load_analysis_features to project an analysis grid") + result = [float(b) for b in beat_grid] + if not result or not np.isfinite(result).all() or np.any(np.diff(result) <= 0): + raise ValueError("Environment beat grid must be nonempty, finite, and strictly increasing") + return result def _generate_synthetic_grid(self, num_beats: int, bpm: float) -> List[float]: # Quarter-beat subdivision return [round(i * 0.25, 4) for i in range(num_beats * 4)] def _generate_synthetic_audio(self, length: int) -> Dict[str, Any]: + stems = ("drums", "bass", "guitar", "piano", "vocals", "other") return { - "onsets": np.random.uniform(0.0, 1.0, size=(length,)).tolist(), - "flux": np.random.uniform(0.0, 1.0, size=(length,)).tolist(), - "stems": { - "drums": np.random.uniform(0.0, 1.0, size=(length,)).tolist(), - "bass": np.random.uniform(0.0, 1.0, size=(length,)).tolist(), - "guitar": np.random.uniform(0.0, 0.5, size=(length,)).tolist(), - "piano": np.random.uniform(0.0, 0.5, size=(length,)).tolist(), - "vocals": np.random.uniform(0.0, 0.8, size=(length,)).tolist(), - "other": np.random.uniform(0.0, 0.5, size=(length,)).tolist(), - }, - "sections": ["verse"] * length, + "onsets": [1.0 if i % 4 == 0 else 0.0 for i in range(length)], + "flux": [0.0] * length, + "stems": {stem: [0.0] * length for stem in stems}, + "stemAvailability": {stem: False for stem in stems}, + "sections": ["unknown"] * length, + "provenance": {"source": "synthetic-test-pulse", "timingStatus": "synthetic"}, } def reset( @@ -142,32 +142,34 @@ def reset( difficulty: Optional[str] = None, ) -> Tuple[np.ndarray, Dict[str, Any]]: if seed is not None: - np.random.seed(seed) + self.rng = np.random.default_rng(seed) if bpm is not None: - self.bpm = max(1.0, float(bpm)) + if not math.isfinite(float(bpm)) or float(bpm) <= 0: + raise ValueError("RL BPM must be finite and positive") + self.bpm = self.base_bpm = float(bpm) if difficulty is not None and difficulty in DIFFICULTIES: self.difficulty = difficulty self.target_nps = TARGET_NPS_MAP.get(self.difficulty, 4.25) - self.reward_engine = CompositeReward( - difficulty=self.difficulty, - bpm=self.bpm, - target_nps=self.target_nps, - ) + self.reward_engine = CompositeReward( + difficulty=self.difficulty, bpm=self.bpm, target_nps=self.target_nps, + ) if beat_grid is not None: self.beat_grid = self._normalize_beat_grid(beat_grid) elif audio_features is not None: - self.beat_grid = self._generate_synthetic_grid(len(audio_features.get("onsets", [])), self.bpm) + self.beat_grid = self._normalize_beat_grid([i * 0.25 for i in range(len(audio_features.get("onsets", [])))]) if audio_features is not None: self.audio_features = audio_features self.current_step = 0 self.total_steps = len(self.beat_grid) + self.validation_clock = ValidationClock(self.audio_features, self.beat_grid, self.base_bpm) self.red_prev = None self.blue_prev = None self.placed_notes = [] + self._update_step_tempo() obs = self._get_observation() info = {"current_step": 0, "beat": self.beat_grid[0] if self.beat_grid else 0.0} @@ -175,10 +177,12 @@ def reset( def action_masks(self) -> Tuple[np.ndarray, np.ndarray]: """Compute binary action masks for Left (Red) and Right (Blue) hands.""" + self._update_step_tempo() mask_red = np.ones(NUM_HAND_ACTIONS, dtype=bool) mask_blue = np.ones(NUM_HAND_ACTIONS, dtype=bool) current_beat = self.beat_grid[self.current_step] if self.current_step < self.total_steps else 0.0 + validation_beat = self.validation_clock.beat(current_beat) recovery_beats = RECOVERY_BEATS.get(self.difficulty, 0.30) # Rhythmic grid subdivision gating @@ -193,7 +197,7 @@ def action_masks(self) -> Tuple[np.ndarray, np.ndarray]: return mask_red, mask_blue # 1. Mask Left Hand (Red, Color 0) - if self.red_prev is not None and (current_beat - float(self.red_prev["b"])) < (recovery_beats - 1e-5): + if self.red_prev is not None and (validation_beat - self.validation_clock.beat(float(self.red_prev["b"]))) < recovery_beats: mask_red[1:] = False else: for act in range(1, NUM_HAND_ACTIONS): @@ -204,14 +208,11 @@ def action_masks(self) -> Tuple[np.ndarray, np.ndarray]: candidate = {"b": current_beat, "c": 0, "x": x, "y": y, "d": d} if self.red_prev is not None: - finding = transition_finding( - self.red_prev, candidate, self.bpm, recovery_beats=recovery_beats - ) - if finding is not None: + if not transition_is_safe(self.validation_clock.note(self.red_prev), self.validation_clock.note(candidate), self.base_bpm, self.difficulty): mask_red[act] = False # 2. Mask Right Hand (Blue, Color 1) - if self.blue_prev is not None and (current_beat - float(self.blue_prev["b"])) < (recovery_beats - 1e-5): + if self.blue_prev is not None and (validation_beat - self.validation_clock.beat(float(self.blue_prev["b"]))) < recovery_beats: mask_blue[1:] = False else: for act in range(1, NUM_HAND_ACTIONS): @@ -222,14 +223,27 @@ def action_masks(self) -> Tuple[np.ndarray, np.ndarray]: candidate = {"b": current_beat, "c": 1, "x": x, "y": y, "d": d} if self.blue_prev is not None: - finding = transition_finding( - self.blue_prev, candidate, self.bpm, recovery_beats=recovery_beats - ) - if finding is not None: + if not transition_is_safe(self.validation_clock.note(self.blue_prev), self.validation_clock.note(candidate), self.base_bpm, self.difficulty): mask_blue[act] = False return mask_red, mask_blue + def blue_action_mask(self, red_action: int, base_mask: Optional[np.ndarray] = None) -> np.ndarray: + """Condition the second hand on the first hand's actual selected action.""" + mask = (base_mask if base_mask is not None else self.action_masks()[1]).copy() + decoded = decode_hand_action(red_action) + if decoded is None or self.current_step >= self.total_steps: + return mask + beat = self.beat_grid[self.current_step] + red = {"b": beat, "c": 0, "x": decoded[0], "y": decoded[1], "d": decoded[2]} + for action in np.flatnonzero(mask[1:]) + 1: + x, y, direction = decode_hand_action(int(action)) + blue = {"b": beat, "c": 1, "x": x, "y": y, "d": direction} + if cross_hand_finding(red, blue) is not None or vision_blocking_double(red, blue): + mask[action] = False + mask[ACTION_IDLE] = True + return mask + def step( self, action: Tuple[int, int] ) -> Tuple[np.ndarray, float, bool, bool, Dict[str, Any]]: @@ -240,6 +254,7 @@ def step( act_red, act_blue = action current_beat = self.beat_grid[self.current_step] + self._update_step_tempo() # Decode actions red_decoded = decode_hand_action(act_red) @@ -269,16 +284,16 @@ def step( # Compute Step Reward breakdown: RewardBreakdown = self.reward_engine.compute_step_reward( - beat=current_beat, - red_prev=self.red_prev, - red_curr=red_curr, - blue_prev=self.blue_prev, - blue_curr=blue_curr, + beat=self.validation_clock.beat(current_beat), + red_prev=self.validation_clock.note(self.red_prev), + red_curr=self.validation_clock.note(red_curr), + blue_prev=self.validation_clock.note(self.blue_prev), + blue_curr=self.validation_clock.note(blue_curr), onset_strength=onset, stem_energy=stem_energy, is_downbeat=is_downbeat, section_type=section_type, - recent_notes=self.placed_notes[-self.history_window:], + recent_notes=[self.validation_clock.note(note) for note in self.placed_notes[-self.history_window:]], current_nps=current_nps, target_nps=self.target_nps, ) @@ -292,6 +307,7 @@ def step( self.blue_prev = blue_curr self.current_step += 1 + self._update_step_tempo() terminated = self.current_step >= self.total_steps truncated = False @@ -310,6 +326,13 @@ def _get_audio_field(self, field: str, idx: int, default: Any) -> Any: return arr[idx] return default + def _update_step_tempo(self) -> None: + if not self.total_steps: + return + value = self._get_audio_field("stepBpms", min(self.current_step, self.total_steps - 1), self.bpm) + self.bpm = float(value) + self.reward_engine.kinematic.bpm = self.base_bpm + def _get_stem_energy(self, stem: str, idx: int) -> float: stems = self.audio_features.get("stems", {}) arr = stems.get(stem) @@ -324,7 +347,11 @@ def _compute_rolling_nps(self) -> float: span_beats = float(recent[-1]["b"]) - float(recent[0]["b"]) if span_beats <= 1e-6: return 0.0 - span_seconds = span_beats * 60.0 / self.bpm + times = self.audio_features.get("timesSeconds") + if times is not None and len(times) == self.total_steps: + span_seconds = float(np.interp(float(recent[-1]["b"]), self.beat_grid, times) - np.interp(float(recent[0]["b"]), self.beat_grid, times)) + else: + span_seconds = span_beats * 60.0 / self.bpm return len(recent) / max(0.1, span_seconds) def _get_observation(self) -> np.ndarray: diff --git a/skills/beat-saber-mapping/scripts/rl/evaluate.py b/skills/beat-saber-mapping/scripts/rl/evaluate.py new file mode 100644 index 0000000..e11683f --- /dev/null +++ b/skills/beat-saber-mapping/scripts/rl/evaluate.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Offline held-out checkpoint evaluation. No downloads, training, or installation.""" + +from __future__ import annotations + +import argparse +from collections import Counter +from datetime import datetime, timezone +import json +import hashlib +from pathlib import Path +from typing import Any + +import numpy as np + +from beatforge_core import ValidationReport +from validate_map import validate_v3 +from timing_export import project_map_timing +from rl.features import AnalysisFeatures, FEATURE_SCHEMA, file_sha256 +from rl.library import load_library + + +def chart_metrics(chart: dict[str, Any], bundle: AnalysisFeatures, difficulty: str) -> dict[str, Any]: + """Report measurable chart properties, without assigning a subjective score.""" + report = ValidationReport() + exported = project_map_timing(chart, bundle.analysis) + structural = validate_v3(exported, f"{difficulty}Standard.dat", report, bpm=bundle.bpm, difficulty=difficulty) + notes = sorted(chart.get("colorNotes", []), key=lambda note: (note["b"], note["c"])) + if not notes: + report.add("error", "EMPTY_CHART", "A playable chart must contain color notes") + duration = bundle.provenance["durationSamples"] / bundle.provenance["sampleRate"] + hand_counts = Counter(int(note["c"]) for note in notes) + transitions = [] + for color in (0, 1): + hand = [note for note in notes if note["c"] == color] + transitions.extend((left["x"], left["y"], left["d"], right["x"], right["y"], right["d"]) + for left, right in zip(hand, hand[1:])) + pattern_counts = Counter(transitions) + labels = bundle.audio_features["sections"] + times = bundle.audio_features["timesSeconds"] + note_onsets = [] + section_counts: Counter[str] = Counter() + for note in notes: + index = int(np.clip(np.searchsorted(bundle.beat_grid, note["b"]), 0, len(labels) - 1)) + section_counts[labels[index]] += 1 + note_onsets.append(bundle.audio_features["onsets"][index]) + section_seconds: Counter[str] = Counter() + for index, label in enumerate(labels): + end = times[index + 1] if index + 1 < len(times) else duration + section_seconds[label] += max(0.0, end - times[index]) + return { + "notes": len(notes), "nps": len(notes) / duration, + "redNotes": hand_counts[0], "blueNotes": hand_counts[1], + "handImbalance": abs(hand_counts[0] - hand_counts[1]) / max(1, len(notes)), + "mostRepeatedTransitionFraction": max(pattern_counts.values(), default=0) / max(1, len(transitions)), + "meanOnsetStrengthAtNotes": float(np.mean(note_onsets)) if note_onsets else 0.0, + "sectionNps": {label: section_counts[label] / seconds for label, seconds in section_seconds.items() if seconds > 0}, + "hardErrors": len(report.errors), "warnings": len(report.warnings), + "errorCodes": dict(Counter(issue.code for issue in report.errors)), + "structural": structural, + "headsetPlaytest": "not_performed", + } + + +def evaluate_checkpoints( + manifest: Path, checkpoints: list[Path], *, split: str = "validation", + difficulty: str = "Expert", seed: int = 0, +) -> dict[str, Any]: + from rl.policy_generator import RLMapGenerator + import torch + + if split not in {"validation", "test"}: + raise ValueError("Evaluation must use a held-out validation or test split") + library = load_library(manifest) + held_out = [(track, bundle) for track, bundle in library if track["split"] == split] + if not held_out: + raise ValueError(f"Manifest has no {split} tracks") + results = [] + for checkpoint in checkpoints: + generator = RLMapGenerator(model_path=Path(checkpoint)) + training = generator.checkpoint_metadata.get("trainingSourceSha256") + if not training: + raise ValueError("Checkpoint has no training audio hashes; held-out independence cannot be verified") + if generator.checkpoint_metadata.get("featureSchema") != FEATURE_SCHEMA: + raise ValueError("Held-out comparison requires the current shared feature schema") + if set(training) & {bundle.provenance["sourceSha256"] for _, bundle in held_out}: + raise ValueError("Held-out split contains audio used to train this checkpoint") + tracks = [] + for track, bundle in held_out: + chart = generator.generate_difficulty(bundle.audio_features, bundle.beat_grid, bundle.bpm, + difficulty=difficulty, deterministic=True, seed=seed) + tracks.append({"id": track["id"], "features": bundle.provenance, + "chartSha256": hashlib.sha256(json.dumps(chart, sort_keys=True).encode()).hexdigest(), + "metrics": chart_metrics(chart, bundle, difficulty)}) + results.append({ + "checkpoint": generator.checkpoint_metadata, "tracks": tracks, + "summary": {"tracks": len(tracks), "hardErrors": sum(t["metrics"]["hardErrors"] for t in tracks), + "meanNps": float(np.mean([t["metrics"]["nps"] for t in tracks])), + "tracksWithoutHardErrors": sum(t["metrics"]["hardErrors"] == 0 for t in tracks)}, + }) + return { + "schemaVersion": 1, "kind": "offline-held-out-checkpoint-evaluation", + "createdAt": datetime.now(timezone.utc).isoformat(), "seed": seed, + "deterministic": True, "split": split, "difficulty": difficulty, + "manifestSha256": file_sha256(manifest), "featureSchema": FEATURE_SCHEMA, + "runtime": {"torch": str(torch.__version__), "numpy": str(np.__version__)}, + "evaluatorSha256": { + name: file_sha256(Path(__file__).parent / name) + for name in ("evaluate.py", "features.py", "environment.py", "policy_generator.py", "models.py", "rewards.py", "safety.py") + } | { + name: file_sha256(Path(__file__).parent.parent / name) + for name in ("validate_map.py", "kinematics.py", "safety_contract.py", "timing_export.py", "beatforge_core.py") + }, + "heldOutVerifiedAgainstRecordedTraining": True, + "limitations": ["Recorded hashes establish data separation; they do not prove prior undocumented training history.", + "Chart validation does not replace a complete packaged-map or headset playtest.", + "Musical preference and manual correction counts require human evaluation."], + "results": results, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--checkpoint", type=Path, action="append", required=True) + parser.add_argument("--split", choices=("validation", "test"), default="validation") + parser.add_argument("--difficulty", choices=("Easy", "Normal", "Hard", "Expert", "ExpertPlus"), default="Expert") + parser.add_argument("--seed", type=int, default=0) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + try: + result = evaluate_checkpoints(args.manifest, args.checkpoint, split=args.split, difficulty=args.difficulty, seed=args.seed) + except (ValueError, FileNotFoundError) as exc: + parser.error(str(exc)) + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(json.dumps(result, indent=2, allow_nan=False) + "\n", encoding="utf-8") + print(f"Saved {len(result['results'])} checkpoint evaluation(s) to {args.out}") + return 2 if any(item["summary"]["hardErrors"] for item in result["results"]) else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/beat-saber-mapping/scripts/rl/features.py b/skills/beat-saber-mapping/scripts/rl/features.py new file mode 100644 index 0000000..8b418d7 --- /dev/null +++ b/skills/beat-saber-mapping/scripts/rl/features.py @@ -0,0 +1,219 @@ +"""One sample-aligned feature contract for RL training, evaluation, and generation. + +Consumes the existing analyzer artifacts. It never estimates or verifies timing. +Missing separated stems are zeros with availability recorded, never mix copies. +""" + +from __future__ import annotations + +from dataclasses import dataclass +import hashlib +import json +import math +from pathlib import Path +from typing import Any + +import numpy as np + +FEATURE_SCHEMA = "beatforge-rl-sample-grid-v2" +STEMS = ("drums", "bass", "guitar", "piano", "vocals", "other") + + +@dataclass +class AnalysisFeatures: + audio_features: dict[str, Any] + beat_grid: list[float] + bpm: float + provenance: dict[str, Any] + analysis: dict[str, Any] + + +def file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _unit(values: np.ndarray) -> np.ndarray: + values = np.maximum(values, 0.0) + scale = max(1.0, float(np.max(values))) if values.size else 1.0 + return np.clip(values / scale, 0.0, 1.0) + + +def build_analysis_features( + analysis: dict[str, Any], + grid_document: dict[str, Any], + sections_document: dict[str, Any], + frames: dict[str, Any], + *, + require_verified: bool = True, +) -> AnalysisFeatures: + """Project frames and onset stem annotations onto actual quarter-beat samples.""" + if not all(isinstance(document, dict) for document in (analysis, grid_document, sections_document, frames)): + raise ValueError("RL analysis artifacts must contain objects") + status = str(analysis.get("status", "unknown")) + if require_verified and status != "timing_verified": + raise ValueError(f"RL requires verified analysis; timing status is {status}") + if grid_document.get("status", status) != status: + raise ValueError("Analysis and beat grid timing statuses disagree") + sample_rate = int(analysis["sampleRate"]) + if float(analysis["sampleRate"]) != sample_rate or float(grid_document.get("sampleRate", sample_rate)) != sample_rate: + raise ValueError("Analysis sample rate must be an integer") + if sample_rate != 44100 or int(grid_document.get("sampleRate", sample_rate)) != sample_rate: + raise ValueError("RL analysis must use the canonical 44100 Hz sample clock") + duration = int(analysis["durationSamples"]) + if float(analysis["durationSamples"]) != duration: + raise ValueError("Analysis durationSamples must be an integer") + bpm = float(analysis["bpm"]) + if duration <= 0 or not math.isfinite(bpm) or bpm <= 0: + raise ValueError("Analysis has invalid duration or BPM") + rows = grid_document.get("beats", []) + if len(rows) < 2 or not all(isinstance(row, dict) for row in rows): + raise ValueError("beat_grid.json must contain at least two beat/sample rows") + beats = np.asarray([row["beat"] for row in rows], dtype=float) + raw_samples = np.asarray([row["sample"] for row in rows], dtype=float) + if (not np.isfinite(beats).all() or not np.isfinite(raw_samples).all() + or np.any(raw_samples != np.round(raw_samples)) + or np.any(np.diff(beats) <= 0) or np.any(np.diff(raw_samples) <= 0)): + raise ValueError("Beat/sample rows must be finite, integer-sampled, and strictly increasing") + if raw_samples[0] < 0 or raw_samples[-1] >= duration: + raise ValueError("Beat samples fall outside the analyzed audio") + if "beatGrid" in analysis: + recorded = [(row["beat"], row["sample"]) for row in analysis["beatGrid"]] + if recorded != [(row["beat"], row["sample"]) for row in rows]: + raise ValueError("Analysis and beat_grid.json contain different adopted timing grids") + + # The analyzer may provide finer subdivisions. Select the quarter-beat domain + # through interpolation in the adopted sample clock, including pickups. + first_tick = math.ceil(beats[0] * 4 - 1e-7) + last_tick = math.floor(beats[-1] * 4 + 1e-7) + steps = np.arange(first_tick, last_tick + 1, dtype=float) / 4.0 + if not len(steps): + raise ValueError("Beat grid contains no quarter-beat decision steps") + samples = np.rint(np.interp(steps, beats, raw_samples)).astype(np.int64) + if np.any(np.diff(samples) <= 0): + raise ValueError("Quarter-beat sample positions must increase") + times = samples / sample_rate + + def frame_values(key: str) -> np.ndarray: + values = np.asarray(frames.get(key, []), dtype=float) + if values.ndim != 1: + raise ValueError(f"Audio feature {key} must be a one-dimensional envelope") + if not np.isfinite(values).all(): + raise ValueError(f"Non-finite audio feature {key}") + return values + + def align(key: str) -> list[float]: + values = frame_values(key) + if not len(values): + return [0.0] * len(steps) + if "samples" in frames: + positions = np.asarray(frames["samples"], dtype=float).reshape(-1) + if np.any(positions != np.round(positions)): + raise ValueError("Audio frame sample positions must be integers") + elif "times" in frames or "timeSeconds" in frames: + positions = np.asarray(frames.get("times", frames.get("timeSeconds")), dtype=float).reshape(-1) * sample_rate + else: + hops = np.asarray(frames.get("hopSamples", frames.get("hop", 0))).reshape(-1) + if len(hops) != 1: + raise ValueError("Audio feature frames require a single hopSamples value") + hop = float(hops[0]) + if not math.isfinite(hop) or hop <= 0 or hop != round(hop): + raise ValueError("Audio feature frames require sample positions, times, or integer hopSamples") + positions = np.arange(len(values)) * hop + if len(positions) != len(values) or not np.isfinite(positions).all() or np.any(np.diff(positions) <= 0): + raise ValueError(f"Invalid frame clock for {key}") + return np.interp(samples, positions, _unit(values), left=0.0, right=0.0).tolist() + + onset_key = next((key for key in ("combinedOnset", "combined", "onsets") if key in frames), None) + if onset_key is None: + raise ValueError("Audio feature artifact is missing its onset envelope") + if not len(frame_values(onset_key)): + raise ValueError("Audio feature artifact contains an empty onset envelope") + onsets = align(onset_key) + flux_available = "flux" in frames and bool(len(frame_values("flux"))) + flux = align("flux") if flux_available else [0.0] * len(steps) + + stems = {stem: [0.0] * len(steps) for stem in STEMS} + available = {stem: False for stem in STEMS} + stem_sources: dict[str, str] = {} + sustains = [0.0] * len(steps) + # Demucs is persisted as event annotations after temporary WAVs are removed. + # Assign an event to its nearest step only; never extend it across silence. + for event in analysis.get("events", []): + energies = event.get("stemEnergy", {}) + sample_value = float(event["sample"]) + if not math.isfinite(sample_value) or sample_value != round(sample_value) or not 0 <= sample_value < duration: + raise ValueError("Analysis onset samples must be integers within the audio") + sample = int(sample_value) + right = min(int(np.searchsorted(samples, sample)), len(samples) - 1) + left = max(0, right - 1) + index = left if abs(int(samples[left]) - sample) <= abs(int(samples[right]) - sample) else right + sustain = float(event.get("sustainBeats", 0.0)) + if not math.isfinite(sustain) or sustain < 0: + raise ValueError("Analysis sustain duration must be finite and nonnegative") + sustains[index] = max(sustains[index], sustain) + for stem in STEMS: + if stem not in energies: + continue + value = float(energies[stem]) + if not math.isfinite(value) or value < 0: + raise ValueError(f"Invalid separated {stem} energy") + available[stem] = True + stem_sources[stem] = "analysis.events.stemEnergy" + stems[stem][index] = max(stems[stem][index], min(value, 1.0)) + + labels = ["unknown"] * len(steps) + for section in sections_document.get("sections", []): + # External segment beat fields may have used a constant BPM conversion. + # Prefer sample/second boundaries to retain offset and tempo changes. + if "startSample" in section and "endSample" in section: + selected = (samples >= int(section["startSample"])) & (samples < int(section["endSample"])) + elif "startSeconds" in section and "endSeconds" in section: + selected = (times >= float(section["startSeconds"])) & (times < float(section["endSeconds"])) + elif "startBeat" in section and "endBeat" in section: + selected = (steps >= float(section["startBeat"])) & (steps < float(section["endBeat"])) + else: + raise ValueError("Section requires a start/end interval") + label = str(section.get("label", section.get("type", "unknown"))) + for index in np.flatnonzero(selected): + labels[int(index)] = label + + intervals = np.searchsorted(beats, steps, side="right") - 1 + intervals = np.clip(intervals, 0, len(beats) - 2) + bpms = 60.0 * sample_rate * np.diff(beats)[intervals] / np.diff(raw_samples)[intervals] + provenance = { + "featureSchema": FEATURE_SCHEMA, "timingStatus": status, + "gridSource": grid_document.get("source", analysis.get("gridSource", "unknown")), + "sourceSha256": analysis.get("sourceSha256"), "sampleRate": sample_rate, + "durationSamples": duration, "decisionSteps": len(steps), + "firstSample": int(samples[0]), "lastSample": int(samples[-1]), + "stemAvailability": available, "stemSources": stem_sources, + "fluxAvailable": flux_available, + "sourceAudioVerified": False, + "sectionSource": sections_document.get("source", "analysis"), + } + audio_features = { + "onsets": onsets, "flux": flux, "stems": stems, "sections": labels, + "sustainBeats": sustains, + "samplePositions": samples.tolist(), "timesSeconds": times.tolist(), + "stepBpms": bpms.tolist(), "stemAvailability": available, + "provenance": provenance, + } + return AnalysisFeatures(audio_features, steps.tolist(), bpm, provenance, {**analysis, "beatGrid": rows}) + + +def load_analysis_features(analysis_dir: Path, *, require_verified: bool = True) -> AnalysisFeatures: + analysis_dir = Path(analysis_dir) + names = ("analysis.json", "beat_grid.json", "sections.json", "audio_features.npz") + for name in names: + if not (analysis_dir / name).is_file(): + raise FileNotFoundError(f"Missing RL analysis artifact: {analysis_dir / name}. Run analyze_audio.py first.") + documents = [json.loads((analysis_dir / name).read_text(encoding="utf-8")) for name in names[:3]] + with np.load(analysis_dir / names[3], allow_pickle=False) as archive: + frames = {key: archive[key] for key in archive.files} + result = build_analysis_features(*documents, frames, require_verified=require_verified) + result.provenance["artifactSha256"] = {name: file_sha256(analysis_dir / name) for name in names} + return result diff --git a/skills/beat-saber-mapping/scripts/rl/library.py b/skills/beat-saber-mapping/scripts/rl/library.py new file mode 100644 index 0000000..153b4f1 --- /dev/null +++ b/skills/beat-saber-mapping/scripts/rl/library.py @@ -0,0 +1,46 @@ +"""Explicit training and held-out song lists, identified by audio content hash.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from .features import AnalysisFeatures, file_sha256, load_analysis_features + + +def load_library(manifest: Path) -> list[tuple[dict[str, Any], AnalysisFeatures]]: + manifest = Path(manifest).resolve() + document = json.loads(manifest.read_text(encoding="utf-8")) + if document.get("schemaVersion") != 1 or not isinstance(document.get("tracks"), list): + raise ValueError("Library manifest requires schemaVersion 1 and a tracks list") + result = [] + identities: set[str] = set() + hashes: set[str] = set() + for track in document["tracks"]: + identity = str(track["id"]) + if identity in identities: + raise ValueError(f"Duplicate track ID: {identity}") + if track.get("split") not in {"train", "validation", "test"}: + raise ValueError(f"Track {identity} requires split train, validation, or test") + path = Path(track["analysisDir"]) + path = path if path.is_absolute() else manifest.parent / path + bundle = load_analysis_features(path) + source = bundle.provenance.get("sourceSha256") + if not isinstance(source, str) or len(source) != 64 or any(c not in "0123456789abcdef" for c in source.lower()): + raise ValueError(f"Track {identity} requires an analyzed audio SHA-256") + source = source.lower() + if track.get("audioPath"): + audio = Path(track["audioPath"]) + audio = audio if audio.is_absolute() else manifest.parent / audio + if file_sha256(audio) != source: + raise ValueError(f"Audio content changed since analysis: {identity}") + bundle.provenance["sourceAudioVerified"] = True + if source in hashes: + raise ValueError(f"Duplicate audio content or split leakage: {identity}") + hashes.add(source) + identities.add(identity) + result.append(({**track, "analysisDir": str(path.resolve())}, bundle)) + if not result: + raise ValueError("Library manifest contains no tracks") + return result diff --git a/skills/beat-saber-mapping/scripts/rl/models.py b/skills/beat-saber-mapping/scripts/rl/models.py index 187352b..1b17b8e 100644 --- a/skills/beat-saber-mapping/scripts/rl/models.py +++ b/skills/beat-saber-mapping/scripts/rl/models.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Dict, List, Optional, Tuple +from typing import Callable, Dict, List, Optional, Tuple import torch import torch.nn as nn @@ -103,6 +103,7 @@ def get_action_and_value( action: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, mask_red: Optional[torch.Tensor] = None, mask_blue: Optional[torch.Tensor] = None, + blue_mask_fn: Optional[Callable[[torch.Tensor], torch.Tensor]] = None, ) -> Tuple[Tuple[torch.Tensor, torch.Tensor], torch.Tensor, torch.Tensor, torch.Tensor]: """Compute action sampling, log probability, entropy, and state value.""" features = self.encoder(obs) @@ -125,6 +126,9 @@ def get_action_and_value( logits_blue = self.actor_blue(blue_in) # Apply Action Masking for Blue Hand + if blue_mask_fn is not None: + conditional_mask = blue_mask_fn(act_red) + mask_blue = conditional_mask if mask_blue is None else mask_blue & conditional_mask if mask_blue is not None: logits_blue = torch.where(mask_blue, logits_blue, torch.tensor(-1e8, device=logits_blue.device)) @@ -147,6 +151,8 @@ def predict( mask_red: Optional[torch.Tensor] = None, mask_blue: Optional[torch.Tensor] = None, deterministic: bool = False, + blue_mask_fn: Optional[Callable[[torch.Tensor], torch.Tensor]] = None, + generator: Optional[torch.Generator] = None, ) -> Tuple[int, int]: """Inference action selection.""" self.eval() @@ -166,19 +172,20 @@ def predict( if deterministic: act_red = torch.argmax(logits_red, dim=-1) else: - dist_red = Categorical(logits=logits_red) - act_red = dist_red.sample() + act_red = torch.multinomial(torch.softmax(logits_red, dim=-1), 1, generator=generator).squeeze(-1) red_emb = self.red_action_embed(act_red) blue_in = torch.cat([features, red_emb], dim=-1) logits_blue = self.actor_blue(blue_in) + if blue_mask_fn is not None: + conditional_mask = blue_mask_fn(act_red) + mask_blue = conditional_mask if mask_blue is None else mask_blue & conditional_mask if mask_blue is not None: logits_blue = torch.where(mask_blue, logits_blue, torch.tensor(-1e8, device=logits_blue.device)) if deterministic: act_blue = torch.argmax(logits_blue, dim=-1) else: - dist_blue = Categorical(logits=logits_blue) - act_blue = dist_blue.sample() + act_blue = torch.multinomial(torch.softmax(logits_blue, dim=-1), 1, generator=generator).squeeze(-1) return int(act_red.item()), int(act_blue.item()) diff --git a/skills/beat-saber-mapping/scripts/rl/policy_generator.py b/skills/beat-saber-mapping/scripts/rl/policy_generator.py index ae83cfe..78e9fff 100644 --- a/skills/beat-saber-mapping/scripts/rl/policy_generator.py +++ b/skills/beat-saber-mapping/scripts/rl/policy_generator.py @@ -10,6 +10,9 @@ import numpy as np import torch +from .checkpoints import load_checkpoint +from .safety import safe_hold_candidates +from safety_contract import MIN_CHAIN_DURATION_BEATS try: from .environment import ( @@ -40,11 +43,11 @@ def __init__( self.device = device if policy is not None: self.policy = policy - elif model_path is not None and model_path.exists(): - self.policy = ActorCriticPolicy() - self.policy.load_state_dict(torch.load(model_path, map_location=device)) + self.checkpoint_metadata = {"source": "supplied-policy", "checkpointSha256": None} + elif model_path is not None: + self.policy, self.checkpoint_metadata = load_checkpoint(model_path, device) else: - self.policy = ActorCriticPolicy() + raise ValueError("RL generation requires an explicit trained checkpoint (--rl-model)") self.policy.to(device) self.policy.eval() @@ -56,6 +59,7 @@ def generate_difficulty( bpm: float, difficulty: str = "Expert", deterministic: bool = True, + seed: int = 0, ) -> Dict[str, Any]: """Generate a complete v3.3.0 difficulty map using the RL policy.""" env = BeatSaberEnv( @@ -64,7 +68,8 @@ def generate_difficulty( bpm=bpm, difficulty=difficulty, ) - obs, _ = env.reset() + random = torch.Generator(device=self.device).manual_seed(seed) + obs, _ = env.reset(seed=seed) color_notes: List[Dict[str, Any]] = [] bomb_notes: List[Dict[str, Any]] = [] @@ -80,7 +85,11 @@ def generate_difficulty( t_mask_b = torch.tensor(mask_blue, dtype=torch.bool, device=self.device).unsqueeze(0) act_red, act_blue = self.policy.predict( - t_obs, mask_red=t_mask_r, mask_blue=t_mask_b, deterministic=deterministic + t_obs, mask_red=t_mask_r, mask_blue=t_mask_b, deterministic=deterministic, + blue_mask_fn=lambda selected: torch.as_tensor( + env.blue_action_mask(int(selected.item()), mask_blue), dtype=torch.bool, device=self.device + ).unsqueeze(0), + generator=random, ) current_beat = round(beat_grid[step], 4) @@ -116,10 +125,13 @@ def generate_difficulty( break # Generate Arcs (Sliders) connecting smooth swing trajectories (Post-2022 v3 feature) - sliders = self._generate_arcs(color_notes) + sliders = self._generate_arcs(color_notes, audio_features, beat_grid) # Generate Chains (Burst Sliders) on sustained vibration onsets (Post-2022 v3 feature) burst_sliders = self._generate_chains(color_notes, audio_features, beat_grid) + sliders, burst_sliders, hold_relaxations = safe_hold_candidates( + color_notes, sliders, burst_sliders, audio_features, beat_grid, bpm, difficulty + ) # Generate framing walls & bombs obstacles, bomb_notes = self._generate_obstacles_and_bombs( @@ -150,11 +162,16 @@ def generate_difficulty( "_provenance": { "poseSolver": "rl-policy-v1", "format": "v3.3.0", + "seed": seed, + "checkpoint": self.checkpoint_metadata, + "features": audio_features.get("provenance", {}), + "holdRelaxations": hold_relaxations, + "holdSelection": "audio-evidence-with-fixed-note-safety", }, }, } - def _generate_arcs(self, color_notes: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def _generate_arcs(self, color_notes: List[Dict[str, Any]], audio_features: Dict[str, Any], beat_grid: List[float]) -> List[Dict[str, Any]]: """Generate rich v3 Arcs / Sliders connecting held phrases and melodic transition swings.""" arcs = [] by_color: Dict[int, List[Dict[str, Any]]] = {0: [], 1: []} @@ -166,9 +183,12 @@ def _generate_arcs(self, color_notes: List[Dict[str, Any]]) -> List[Dict[str, An head = notes[i] tail = notes[i + 1] gap = float(tail["b"]) - float(head["b"]) + index = int(np.searchsorted(beat_grid, float(head["b"]))) + sustains = audio_features.get("sustainBeats", []) + sustained = index < len(sustains) and float(sustains[index]) >= gap # Connect notes spaced 0.5 to 3.5 beats apart on melodic transitions - if 0.5 <= gap <= 3.5 and head["d"] != 8 and tail["d"] != 8 and (i % 2 == 0): + if sustained and 0.5 <= gap <= 3.5 and head["d"] != 8 and tail["d"] != 8 and (i % 2 == 0): control = min(2.0, max(0.8, gap * 0.45)) arcs.append({ "b": float(head["b"]), @@ -216,7 +236,7 @@ def _generate_chains( last_chain_beat = -999.0 for i, note in enumerate(color_notes): b = float(note["b"]) - step_idx = min(int(b * 4), len(onsets) - 1) + step_idx = int(np.searchsorted(beat_grid, b)) drum_val = float(drums[step_idx]) if step_idx >= 0 and step_idx < len(drums) and isinstance(drums, list) else 0.0 vocal_val = float(vocals[step_idx]) if step_idx >= 0 and step_idx < len(vocals) and isinstance(vocals, list) else 0.0 @@ -246,8 +266,8 @@ def _generate_chains( ty = min(2, max(0, hy + chosen_dy)) # Determine duration and slice count - dt = 0.0625 if (tx == hx and ty == hy) else (0.125 if abs(chosen_dx) + abs(chosen_dy) <= 1 else 0.25) - sc = 3 if dt <= 0.0625 else (4 if dt <= 0.125 else 5) + dt = MIN_CHAIN_DURATION_BEATS + sc = 5 chains.append({ "b": b, @@ -309,7 +329,7 @@ def notes_in_window(start_b: float, end_b: float) -> List[Dict[str, Any]]: if beat < last_wall_end + min_wall_gap: continue - step_idx = min(int(beat * 4), len(onsets) - 1) + step_idx = i intensity = onsets[step_idx] if 0 <= step_idx < len(onsets) else 0.5 bass_val = bass_energy[step_idx] if 0 <= step_idx < len(bass_energy) else 0.5 diff --git a/skills/beat-saber-mapping/scripts/rl/registry.py b/skills/beat-saber-mapping/scripts/rl/registry.py new file mode 100644 index 0000000..2122446 --- /dev/null +++ b/skills/beat-saber-mapping/scripts/rl/registry.py @@ -0,0 +1,273 @@ +"""Local checkpoint promotion with measured regression gates and rollback. + +Promotion never trains, downloads, installs maps, or asserts human preference. +Stored checkpoints and reports remain immutable by content hash. +""" + +from __future__ import annotations + +import argparse +from contextlib import contextmanager +from datetime import datetime, timezone +import json +import math +import os +from pathlib import Path +import shutil +from typing import Any + +from rl.environment import TARGET_NPS_MAP +from rl.features import FEATURE_SCHEMA, file_sha256 + +PRIMARY_METRIC = "meanOnsetStrengthAtNotes" +MIN_PRIMARY_GAIN = 0.001 + + +def _read(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def _finite(metrics: dict[str, Any], key: str) -> float: + value = metrics.get(key) + if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value): + raise ValueError(f"Evaluation is missing a finite metric: {key}") + return float(value) + + +def _result(report: dict[str, Any], checkpoint_hash: str) -> dict[str, Any]: + if (report.get("kind") != "offline-held-out-checkpoint-evaluation" + or report.get("schemaVersion") != 1 + or report.get("featureSchema") != FEATURE_SCHEMA + or report.get("heldOutVerifiedAgainstRecordedTraining") is not True + or report.get("split") not in {"validation", "test"} + or report.get("deterministic") is not True): + raise ValueError("Promotion requires a verified held-out evaluation with the current schema") + matches = [item for item in report.get("results", []) + if item.get("checkpoint", {}).get("checkpointSha256") == checkpoint_hash] + if len(matches) != 1: + raise ValueError("Evaluation must identify the exact candidate checkpoint once") + selected = matches[0] + training = selected.get("checkpoint", {}).get("trainingSourceSha256") + if not training or not selected.get("tracks"): + raise ValueError("Evaluation must record training hashes and nonempty held-out tracks") + seen: set[str] = set() + for track in selected["tracks"]: + if track["id"] in seen: + raise ValueError("Evaluation repeats a held-out track ID") + seen.add(track["id"]) + features = track.get("features", {}) + source = features.get("sourceSha256") + if not source or source in training or not features.get("artifactSha256"): + raise ValueError("Evaluation contains training overlap or lacks sample artifact identity") + if features.get("timingStatus") != "timing_verified": + raise ValueError("Promotion requires verified timing on every held-out track") + metrics = track["metrics"] + if _finite(metrics, "hardErrors") != 0: + raise ValueError(f"Candidate has hard validation errors on {track['id']}") + if _finite(metrics, "notes") <= 0: + raise ValueError(f"Candidate has an empty chart on {track['id']}") + for key in (PRIMARY_METRIC, "handImbalance", "mostRepeatedTransitionFraction", "nps", "warnings"): + if _finite(metrics, key) < 0: + raise ValueError(f"Metric cannot be negative: {key}") + return selected + + +def compare_evaluations( + baseline: dict[str, Any], baseline_hash: str, + candidate: dict[str, Any], candidate_hash: str, +) -> dict[str, Any]: + """Reject benchmark changes, regressions, and gains below the fixed threshold.""" + before = _result(baseline, baseline_hash) + after = _result(candidate, candidate_hash) + identity = ("manifestSha256", "featureSchema", "split", "difficulty", "seed", "deterministic", "runtime", "evaluatorSha256") + if any(key not in baseline or key not in candidate or baseline[key] != candidate[key] for key in identity): + raise ValueError("Benchmark identity differs; re-evaluate both checkpoints on the same benchmark") + old = {track["id"]: track for track in before["tracks"]} + new = {track["id"]: track for track in after["tracks"]} + if old.keys() != new.keys(): + raise ValueError("Held-out track sets differ") + target = TARGET_NPS_MAP.get(candidate["difficulty"]) + if target is None: + raise ValueError("Unknown evaluation difficulty") + gains = [] + for identity, current in new.items(): + previous = old[identity] + if current["features"] != previous["features"]: + raise ValueError(f"Held-out audio analysis changed: {identity}") + a, b = previous["metrics"], current["metrics"] + for key in ("handImbalance", "mostRepeatedTransitionFraction", "warnings"): + if _finite(b, key) > _finite(a, key) + 1e-9: + raise ValueError(f"Regression in {key} on {identity}") + if abs(_finite(b, "nps") - target) > abs(_finite(a, "nps") - target) + 1e-9: + raise ValueError(f"Regression in target density on {identity}") + gain = _finite(b, PRIMARY_METRIC) - _finite(a, PRIMARY_METRIC) + if gain < -1e-9: + raise ValueError(f"Regression in onset alignment on {identity}") + gains.append(gain) + mean_gain = sum(gains) / len(gains) + if mean_gain < MIN_PRIMARY_GAIN: + raise ValueError(f"Primary metric must improve by at least {MIN_PRIMARY_GAIN}") + return {"primaryMetric": PRIMARY_METRIC, "meanGain": mean_gain, + "minimumGain": MIN_PRIMARY_GAIN, "tracksCompared": len(gains), + "humanPreferenceEstablished": False} + + +@contextmanager +def _locked(root: Path): + root.mkdir(parents=True, exist_ok=True) + lock = root / ".registry.lock" + try: + descriptor = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + except FileExistsError as exc: + raise ValueError("Registry is locked by another operation; retry after it finishes") from exc + try: + os.close(descriptor) + yield + finally: + lock.unlink() + + +def _save_registry(root: Path, state: dict[str, Any]) -> None: + temporary = root / "registry.json.tmp" + temporary.write_text(json.dumps(state, indent=2, allow_nan=False) + "\n", encoding="utf-8") + temporary.replace(root / "registry.json") + + +def _store_artifact(source: Path, destination: Path, expected: str) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + if destination.exists(): + if file_sha256(destination) != expected: + raise ValueError("Stored registry artifact failed its content hash") + return + temporary = destination.with_suffix(destination.suffix + ".tmp") + shutil.copyfile(source, temporary) + if file_sha256(temporary) != expected: + temporary.unlink() + raise ValueError("Artifact changed during registry import") + temporary.replace(destination) + + +def register_checkpoint(root: Path, checkpoint: Path, evaluation: Path, *, initialize: bool = False) -> dict[str, Any]: + """Install a baseline explicitly, or promote a measured improvement atomically.""" + from rl.checkpoints import load_checkpoint + + root, checkpoint, evaluation = Path(root), Path(checkpoint), Path(evaluation) + _, metadata = load_checkpoint(checkpoint) + digest = metadata["checkpointSha256"] + report = _read(evaluation) + report_hash = file_sha256(evaluation) + selected = _result(report, digest) + if selected["checkpoint"].get("trainingSourceSha256") != metadata.get("trainingSourceSha256"): + raise ValueError("Evaluation training lineage does not match checkpoint metadata") + with _locked(root): + state = _read(root / "registry.json") if (root / "registry.json").exists() else {"schemaVersion": 1, "active": None, "models": {}, "history": []} + active = state["active"] + if initialize: + if active is not None: + raise ValueError("Registry already has a baseline; use promote") + decision = {"baselineInitialized": True, "humanPreferenceEstablished": False} + else: + if active is None: + raise ValueError("Initialize an evaluated baseline before promotion") + if active == digest: + raise ValueError("Candidate is already the active checkpoint") + previous = state["models"][active] + baseline_path = root / previous["evaluation"] + if file_sha256(baseline_path) != previous["evaluationSha256"]: + raise ValueError("Stored baseline evaluation changed") + decision = compare_evaluations(_read(baseline_path), active, report, digest) + checkpoint_name = f"checkpoints/{digest}.pt" + evaluation_name = f"evaluations/{report_hash}.json" + _store_artifact(checkpoint, root / checkpoint_name, digest) + _store_artifact(evaluation, root / evaluation_name, report_hash) + state["models"][digest] = {"checkpoint": checkpoint_name, "evaluation": evaluation_name, + "evaluationSha256": report_hash, "metadata": metadata} + state["history"].append({"action": "initialize" if initialize else "promote", "from": active, "to": digest, + "at": datetime.now(timezone.utc).isoformat(), "decision": decision}) + state["active"] = digest + _save_registry(root, state) + return {"active": digest, "previous": active, "checkpoint": str((root / checkpoint_name).resolve()), "decision": decision} + + +def active_checkpoint(root: Path) -> Path: + root = Path(root) + state = _read(root / "registry.json") + digest = state.get("active") + if not digest: + raise ValueError("Registry has no active checkpoint") + if not isinstance(digest, str) or len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest): + raise ValueError("Registry contains an invalid checkpoint identity") + path = root / "checkpoints" / f"{digest}.pt" + if file_sha256(path) != digest: + raise ValueError("Active checkpoint failed its recorded content hash") + return path + + +def registry_status(root: Path) -> dict[str, Any]: + """Compact read-only studio status; does not import PyTorch or expose paths.""" + root = Path(root) + if not (root / "registry.json").is_file(): + return {"status": "empty", "active": None, "modelCount": 0, "automaticTraining": False} + try: + state = _read(root / "registry.json") + active_checkpoint(root) + model = state["models"][state["active"]] + metadata = model.get("metadata", {}) + history = state.get("history", []) + return { + "status": "ready", "active": state["active"], "modelCount": len(state["models"]), + "featureSchema": metadata.get("featureSchema"), + "trainingTrackCount": len(metadata.get("trainingSourceSha256", [])), + "lastAction": history[-1] if history else None, + "automaticTraining": False, "humanPreferenceEstablished": False, + } + except (ValueError, KeyError, TypeError, OSError) as exc: + return {"status": "invalid", "active": None, "automaticTraining": False, + "error": "Registry or stored checkpoint integrity check failed"} + + +def rollback(root: Path) -> dict[str, Any]: + root = Path(root) + with _locked(root): + state = _read(root / "registry.json") + active = state["active"] + previous = next((entry["from"] for entry in reversed(state["history"]) + if entry["to"] == active and entry["from"] is not None), None) + if previous is None: + raise ValueError("No previous active checkpoint exists") + path = root / "checkpoints" / f"{previous}.pt" + if file_sha256(path) != previous: + raise ValueError("Rollback checkpoint failed its recorded content hash") + state["active"] = previous + state["history"].append({"action": "rollback", "from": active, "to": previous, + "at": datetime.now(timezone.utc).isoformat()}) + _save_registry(root, state) + return {"active": previous, "previous": active, "checkpoint": str(path.resolve())} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--registry", required=True, type=Path) + commands = parser.add_subparsers(dest="command", required=True) + for command in ("initialize", "promote"): + sub = commands.add_parser(command) + sub.add_argument("--checkpoint", type=Path, required=True) + sub.add_argument("--evaluation", type=Path, required=True) + commands.add_parser("rollback") + commands.add_parser("active") + args = parser.parse_args() + try: + if args.command == "active": + result = {"checkpoint": str(active_checkpoint(args.registry).resolve())} + elif args.command == "rollback": + result = rollback(args.registry) + else: + result = register_checkpoint(args.registry, args.checkpoint, args.evaluation, initialize=args.command == "initialize") + except (ValueError, FileNotFoundError) as exc: + parser.error(str(exc)) + print(json.dumps(result, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/beat-saber-mapping/scripts/rl/safety.py b/skills/beat-saber-mapping/scripts/rl/safety.py new file mode 100644 index 0000000..3cb5d19 --- /dev/null +++ b/skills/beat-saber-mapping/scripts/rl/safety.py @@ -0,0 +1,86 @@ +"""RL checks in the same constant-BPM coordinates used by the package validator.""" + +from __future__ import annotations + +import math +from collections import Counter +from typing import Any + +import numpy as np + +from kinematics import transition_finding +from safety_contract import MIN_CHAIN_DURATION_BEATS, RECOVERY_BEATS, occupied_until, same_hand_after_hold_too_soon + + +class ValidationClock: + def __init__(self, audio: dict[str, Any], grid: list[float], bpm: float): + self.grid = grid + self.bpm = bpm + times = audio.get("timesSeconds") + if times is None: + self.exported = list(grid) + else: + if len(times) != len(grid) or not np.isfinite(times).all() or np.any(np.diff(times) <= 0): + raise ValueError("RL time positions must match the grid and increase monotonically") + self.exported = [round(float(time) * bpm / 60.0, 8) for time in times] + + def beat(self, beat: float) -> float: + return float(np.interp(beat, self.grid, self.exported)) + + def note(self, note: dict[str, Any] | None) -> dict[str, Any] | None: + return {**note, "b": self.beat(float(note["b"]))} if note is not None else None + + +def transition_is_safe(previous: dict[str, Any], current: dict[str, Any], bpm: float, difficulty: str) -> bool: + if transition_finding(previous, current, bpm, recovery_beats=RECOVERY_BEATS[difficulty]) is not None: + return False + # validate_v3 also enforces this reach cap independently of the swing model. + # Keep validator-based adversarial tests to detect any future contract drift. + gap = float(current["b"]) - float(previous["b"]) + distance = math.hypot(int(current["x"]) - int(previous["x"]), int(current["y"]) - int(previous["y"])) + return not (gap <= .25 and distance > 2.5) + + +def safe_hold_candidates(notes, arcs, chains, audio, grid, bpm, difficulty): + """Keep only decorations compatible with the already selected hand paths. + + The policy currently selects notes, not holds. Decoration must therefore + respect those notes and must never rewrite a valid swing into an unsafe exit. + """ + clock = ValidationClock(audio, grid, bpm) + hands = {color: sorted((clock.note(note) for note in notes if note["c"] == color), key=lambda note: note["b"]) + for color in (0, 1)} + accepted = {"arc": [], "chain": []} + busy = {0: -math.inf, 1: -math.inf} + rejected = Counter() + proposals = [("arc", item) for item in arcs] + [("chain", item) for item in chains] + for kind, item in sorted(proposals, key=lambda proposal: (proposal[1]["b"], proposal[0])): + color = item["c"] + start, tail = clock.beat(item["b"]), clock.beat(item["tb"]) + if item["tb"] > grid[-1] or tail <= start: + rejected["outside_grid"] += 1 + continue + if kind == "chain" and tail - start < MIN_CHAIN_DURATION_BEATS - 1e-8: + rejected["chain_too_short"] += 1 + continue + if start < busy[color]: + rejected["hold_occupancy"] += 1 + continue + hand = hands[color] + inside = [note for note in hand if start + 1e-6 < note["b"] < tail - 1e-6] + if inside or (kind == "chain" and any(abs(note["b"] - tail) <= 1e-6 for note in hand)): + rejected["note_occupancy"] += 1 + continue + next_note = next((note for note in hand if note["b"] > tail + 1e-6), None) + exit_pose = {"b": tail, "c": color, "x": item["tx"], "y": item["ty"], "d": item.get("tc", item["d"])} + if next_note is not None: + gap = next_note["b"] - tail + if same_hand_after_hold_too_soon(gap) or next_note["b"] < occupied_until(tail, RECOVERY_BEATS[difficulty]): + rejected["post_hold_recovery"] += 1 + continue + if not transition_is_safe(exit_pose, next_note, bpm, difficulty): + rejected["tail_exit_motion"] += 1 + continue + accepted[kind].append(item) + busy[color] = occupied_until(tail, RECOVERY_BEATS[difficulty]) + return accepted["arc"], accepted["chain"], dict(rejected) diff --git a/skills/beat-saber-mapping/scripts/rl/train_custom_tracks.py b/skills/beat-saber-mapping/scripts/rl/train_custom_tracks.py index ddab251..6a56d98 100644 --- a/skills/beat-saber-mapping/scripts/rl/train_custom_tracks.py +++ b/skills/beat-saber-mapping/scripts/rl/train_custom_tracks.py @@ -1,162 +1,122 @@ #!/usr/bin/env python3 -"""Train RL Policy on User Tracks: Daft Punk R.A.M and Ninajirachi.""" +"""Train on verified local analyzer artifacts using the inference feature contract.""" from __future__ import annotations import argparse -import io -import sys from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any -if hasattr(sys.stdout, "reconfigure"): - sys.stdout.reconfigure(encoding="utf-8", errors="replace") -if hasattr(sys.stderr, "reconfigure"): - sys.stderr.reconfigure(encoding="utf-8", errors="replace") - -import numpy as np import torch -try: - from .environment import BeatSaberEnv - from .models import ActorCriticPolicy - from .train_ppo import train_ppo -except ImportError: - from rl.environment import BeatSaberEnv - from rl.models import ActorCriticPolicy - from rl.train_ppo import train_ppo - -from beatforge_core import AudioBuffer, frame_features, load_audio - - -def extract_track_features(audio_path: Path) -> Tuple[Dict[str, Any], List[float], float]: - """Extract audio features, beat grid, and BPM for RL environment training.""" - print(f"Extracting features from {audio_path.name}...", flush=True) - audio = load_audio(audio_path) - features = frame_features(audio.samples) - hop = int(features["hop"][0]) - sr = audio.sample_rate +from rl.checkpoints import load_checkpoint +from rl.environment import BeatSaberEnv +from rl.features import file_sha256, load_analysis_features +from rl.library import load_library +from rl.models import ActorCriticPolicy +from rl.train_ppo import train_ppo - flux = features["flux"] - bpm_est = 120.0 # default fallback - # Compute rough tempo / beat grid - length = len(flux) - grid_len = max(32, int(audio.duration_samples / (sr * 0.5))) - beat_grid = [round(i * 0.25, 4) for i in range(grid_len)] +def extract_track_features(audio_path: Path, analysis_dir: Path | None = None) -> tuple[dict[str, Any], list[float], float]: + """Load existing analysis; raw audio must match its recorded source hash. - # Interpolate flux into onsets - onsets = np.interp(np.linspace(0, len(flux), grid_len), np.arange(len(flux)), flux) - onsets = (onsets - np.min(onsets)) / (np.max(onsets) - np.min(onsets) + 1e-6) - - # Simulated stem profiles based on transient frequency bands - audio_features = { - "onsets": onsets.tolist(), - "flux": onsets.tolist(), - "stems": { - "drums": (onsets * 0.8 + np.random.uniform(0.0, 0.2, grid_len)).tolist(), - "bass": (onsets * 0.6 + np.random.uniform(0.0, 0.4, grid_len)).tolist(), - "vocals": (onsets * 0.5 + np.random.uniform(0.0, 0.3, grid_len)).tolist(), - "guitar": (onsets * 0.4 + np.random.uniform(0.0, 0.2, grid_len)).tolist(), - "piano": (onsets * 0.3 + np.random.uniform(0.0, 0.2, grid_len)).tolist(), - "other": (onsets * 0.3 + np.random.uniform(0.0, 0.2, grid_len)).tolist(), - }, - "sections": ["verse"] * grid_len, - } - - return audio_features, beat_grid, bpm_est + A directory can be supplied directly. For an audio file, the default artifact + directory is its sibling ``.analysis``. Analysis and human timing review + run before training, so no background model downloads or fake clocks occur. + """ + audio_path = Path(audio_path) + directory = analysis_dir or (audio_path if audio_path.is_dir() else audio_path.with_suffix(".analysis")) + bundle = load_analysis_features(directory) + if audio_path.is_file() and file_sha256(audio_path) != bundle.provenance.get("sourceSha256"): + raise ValueError(f"Analysis does not belong to audio file {audio_path.name}") + if audio_path.is_file(): + bundle.provenance["sourceAudioVerified"] = True + return bundle.audio_features, bundle.beat_grid, bundle.bpm def train_on_custom_library( - tracks: List[Path], + tracks: list[Path], timesteps_per_track: int = 16384, epochs: int = 4, lr: float = 2e-4, model_path: Path = Path("data/models/ppo_policy.pt"), -) -> None: + *, + rounds: int = 1, + seed: int = 0, + resume: bool = True, + held_out_hashes: set[str] | None = None, +) -> dict[str, Any]: + if not tracks or rounds <= 0: + raise ValueError("Training requires tracks and a positive number of rounds") + # Validate the entire library before updating any weights. + prepared = [(Path(track), extract_track_features(Path(track))) for track in tracks] + training_hashes: set[str] = set() + for track, (features, _, _) in prepared: + source = features["provenance"].get("sourceSha256") + if not isinstance(source, str) or len(source) != 64: + raise ValueError(f"Training analysis requires the audio source SHA-256: {track}") + training_hashes.add(source) + held_out_hashes = held_out_hashes or set() + torch.manual_seed(seed) policy = ActorCriticPolicy() - if model_path.is_file(): - try: - policy.load_state_dict(torch.load(model_path, map_location="cpu")) - print(f"Resumed existing weights from {model_path}", flush=True) - except Exception as e: - print(f"Could not load checkpoint: {e}", flush=True) - - print(f"\n=======================================================", flush=True) - print(f" Starting Continuous RL Training on {len(tracks)} User Tracks", flush=True) - print(f"=======================================================\n", flush=True) - - for round_idx in range(1, 100): - print(f"\n--- Training Round {round_idx} Across Library ---", flush=True) - for idx, track_path in enumerate(tracks, 1): - if not track_path.is_file(): - continue - print(f"\n[{idx}/{len(tracks)}] Processing: {track_path.name}", flush=True) - try: - audio_feats, beat_grid, bpm = extract_track_features(track_path) - env = BeatSaberEnv( - audio_features=audio_feats, - beat_grid=beat_grid, - bpm=bpm, - difficulty="Expert", - ) - train_ppo( - policy=policy, - env=env, - total_timesteps=timesteps_per_track, - rollout_steps=512, - ppo_epochs=epochs, - lr=lr, - save_path=model_path, - ) - except Exception as e: - print(f"Error training on {track_path.name}: {e}", flush=True) + prior: dict[str, Any] = {} + if resume and model_path.is_file(): + policy, prior = load_checkpoint(model_path) + if not prior.get("trainingSourceSha256"): + raise ValueError("Checkpoint has no training-library provenance. Use --no-resume for a traceable run.") + training_hashes.update(prior["trainingSourceSha256"]) + if training_hashes & held_out_hashes: + raise ValueError("Held-out audio appears in the checkpoint or requested training library") + + runs = [] + for round_index in range(rounds): + for index, (track, (features, grid, bpm)) in enumerate(prepared): + run_seed = seed + round_index * len(prepared) + index + print(f"Round {round_index + 1}/{rounds}, track {index + 1}/{len(prepared)}: {track.name}", flush=True) + env = BeatSaberEnv(features, grid, bpm, difficulty="Expert") + metrics = train_ppo( + policy, env, total_timesteps=timesteps_per_track, + rollout_steps=min(512, timesteps_per_track), ppo_epochs=epochs, + lr=lr, save_path=model_path, seed=run_seed, + checkpoint_metadata={ + "trainingSourceSha256": sorted(training_hashes), + "heldOutSourceSha256": sorted(held_out_hashes), + "librarySeed": seed, "round": round_index + 1, + "resumedCheckpointSha256": prior.get("checkpointSha256"), + }, + ) + runs.append({"sourceSha256": features["provenance"]["sourceSha256"], **metrics}) + return {"seed": seed, "rounds": rounds, "runs": runs, "checkpoint": str(model_path)} def main() -> int: - parser = argparse.ArgumentParser(description="Train RL policy on specific tracks") - parser.add_argument("--ram-dir", type=Path, default=Path(r"C:\Users\user\Downloads\R.A.M")) - parser.add_argument("--ninajirachi-dir", type=Path, default=Path(r"data\downloads\ninajirachi")) - parser.add_argument("--spotify-dir", type=Path, default=Path(r"data\downloads\spotify_july")) + parser = argparse.ArgumentParser(description=__doc__) + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument("--analysis-dir", action="append", type=Path, help="Repeat for verified analysis directories") + source.add_argument("--manifest", type=Path, help="JSON song library with explicit train/validation/test splits") parser.add_argument("--timesteps-per-track", type=int, default=16384) + parser.add_argument("--rounds", type=int, default=1) + parser.add_argument("--epochs", type=int, default=4) + parser.add_argument("--lr", type=float, default=2e-4) + parser.add_argument("--seed", type=int, default=0) + parser.add_argument("--resume", action=argparse.BooleanOptionalAction, default=True) parser.add_argument("--out", type=Path, default=Path("data/models/ppo_policy.pt")) args = parser.parse_args() - - tracks: List[Path] = [] - if args.ram_dir.is_dir(): - for ext in ("*.mp3", "*.ogg", "*.wav", "*.m4a"): - tracks.extend(sorted(args.ram_dir.glob(ext))) - - downloads_root = Path("data/downloads") - if downloads_root.is_dir(): - for ext in ("**/*.mp3", "**/*.ogg", "**/*.wav", "**/*.webm", "**/*.m4a"): - tracks.extend(sorted(downloads_root.glob(ext))) - - # Deduplicate paths - unique_tracks = [] - seen = set() - for p in tracks: - res = p.resolve() - if res not in seen and p.is_file(): - seen.add(res) - unique_tracks.append(p) - tracks = unique_tracks - - print(f"Found {len(tracks)} training tracks:") - for t in tracks: - print(f" - {t.name}") - - if not tracks: - print("No audio tracks found!") - return 1 - - train_on_custom_library( - tracks=tracks, - timesteps_per_track=args.timesteps_per_track, - model_path=args.out, - ) + try: + held_out: set[str] = set() + tracks = args.analysis_dir or [] + if args.manifest: + library = load_library(args.manifest) + tracks = [Path(track["analysisDir"]) for track, _ in library if track["split"] == "train"] + held_out = {bundle.provenance["sourceSha256"] for track, bundle in library if track["split"] != "train"} + train_on_custom_library( + tracks, args.timesteps_per_track, args.epochs, args.lr, args.out, + rounds=args.rounds, seed=args.seed, resume=args.resume, held_out_hashes=held_out, + ) + except (ValueError, FileNotFoundError) as exc: + parser.error(str(exc)) return 0 if __name__ == "__main__": - sys.exit(main()) + raise SystemExit(main()) diff --git a/skills/beat-saber-mapping/scripts/rl/train_ppo.py b/skills/beat-saber-mapping/scripts/rl/train_ppo.py index 480b6f0..d315d4c 100644 --- a/skills/beat-saber-mapping/scripts/rl/train_ppo.py +++ b/skills/beat-saber-mapping/scripts/rl/train_ppo.py @@ -12,6 +12,8 @@ import torch import torch.nn as nn import torch.optim as optim +from rl.checkpoints import load_checkpoint, save_checkpoint +from rl.features import load_analysis_features try: from .environment import BeatSaberEnv, NUM_HAND_ACTIONS @@ -70,12 +72,10 @@ def compute_gae(self, last_value: float, gamma: float = 0.99, gae_lambda: float """Compute Generalized Advantage Estimation (GAE).""" last_gae = 0.0 for t in reversed(range(self.ptr)): - if t == self.ptr - 1: - next_val = last_value - next_done = 0.0 - else: - next_val = self.values[t + 1] - next_done = self.dones[t] + next_val = last_value if t == self.ptr - 1 else self.values[t + 1] + # dones[t] belongs to the transition at t, including the last slot. + # After a terminal step the caller's next observation may be a reset. + next_done = self.dones[t] delta = self.rewards[t] + gamma * next_val * (1.0 - next_done) - self.values[t] last_gae = delta + gamma * gae_lambda * (1.0 - next_done) * last_gae @@ -84,7 +84,8 @@ def compute_gae(self, last_value: float, gamma: float = 0.99, gae_lambda: float self.returns[: self.ptr] = self.advantages[: self.ptr] + self.values[: self.ptr] # Normalize advantages valid_adv = self.advantages[: self.ptr] - self.advantages[: self.ptr] = (valid_adv - valid_adv.mean()) / (valid_adv.std() + 1e-8) + if self.ptr: + self.advantages[: self.ptr] = (valid_adv - valid_adv.mean()) / (valid_adv.std(unbiased=False) + 1e-8) def reset(self) -> None: self.ptr = 0 @@ -105,23 +106,30 @@ def train_ppo( vf_coef: float = 0.5, device: str = "cpu", save_path: Optional[Path] = None, + seed: int = 0, + checkpoint_metadata: Optional[Dict[str, Any]] = None, ) -> Dict[str, float]: + if min(total_timesteps, rollout_steps, ppo_epochs, batch_size) <= 0 or lr <= 0: + raise ValueError("Training steps, epochs, batch size, and learning rate must be positive") + torch.manual_seed(seed) + rng = np.random.default_rng(seed) policy.to(device) optimizer = optim.Adam(policy.parameters(), lr=lr, eps=1e-5) buffer = RolloutBuffer(buffer_size=rollout_steps, obs_dim=env.obs_dim, device=device) - obs, _ = env.reset() + obs, _ = env.reset(seed=seed) timesteps_done = 0 iteration = 0 history: Dict[str, List[float]] = {"ep_return": [], "policy_loss": [], "value_loss": []} + current_ep_reward = 0.0 + completed_returns: List[float] = [] while timesteps_done < total_timesteps: buffer.reset() ep_rewards = [] - current_ep_reward = 0.0 policy.eval() - for step in range(rollout_steps): + for step in range(min(rollout_steps, total_timesteps - timesteps_done)): mask_red, mask_blue = env.action_masks() t_obs = torch.tensor(obs, dtype=torch.float32, device=device).unsqueeze(0) t_mask_r = torch.tensor(mask_red, dtype=torch.bool, device=device).unsqueeze(0) @@ -129,15 +137,28 @@ def train_ppo( with torch.no_grad(): (act_red, act_blue), log_prob, _, val = policy.get_action_and_value( - t_obs, mask_red=t_mask_r, mask_blue=t_mask_b + t_obs, mask_red=t_mask_r, mask_blue=t_mask_b, + blue_mask_fn=lambda selected: torch.as_tensor( + env.blue_action_mask(int(selected.item()), mask_blue), dtype=torch.bool, device=device + ).unsqueeze(0), ) a_r = int(act_red.item()) a_b = int(act_blue.item()) + # Replay must use the same conditional distribution as collection. + mask_blue = env.blue_action_mask(a_r, mask_blue) next_obs, reward, terminated, truncated, info = env.step((a_r, a_b)) current_ep_reward += reward + # A time limit is not an MDP terminal. Bootstrap its final observation, + # then cut the advantage trace before the reset episode. + if truncated and not terminated: + with torch.no_grad(): + final_obs = torch.as_tensor(next_obs, dtype=torch.float32, device=device).unsqueeze(0) + final_value = policy.forward(final_obs)[2].item() + reward += gamma * float(final_value) + buffer.add( obs=obs, act_red=a_r, @@ -155,12 +176,13 @@ def train_ppo( if terminated or truncated: ep_rewards.append(current_ep_reward) + completed_returns.append(current_ep_reward) current_ep_reward = 0.0 obs, _ = env.reset() with torch.no_grad(): t_obs = torch.tensor(obs, dtype=torch.float32, device=device).unsqueeze(0) - _, _, _, last_val = policy.get_action_and_value(t_obs) + _, _, last_val = policy.forward(t_obs) last_value = float(last_val.item()) buffer.compute_gae(last_value=last_value, gamma=gamma, gae_lambda=gae_lambda) @@ -173,7 +195,7 @@ def train_ppo( indices = np.arange(buffer.ptr) for epoch in range(ppo_epochs): - np.random.shuffle(indices) + rng.shuffle(indices) for start in range(0, len(indices), batch_size): end = start + batch_size mb_idx = indices[start:end] @@ -229,11 +251,21 @@ def train_ppo( if save_path is not None: save_path.parent.mkdir(parents=True, exist_ok=True) - torch.save(policy.state_dict(), save_path) + metadata = dict(checkpoint_metadata or {}) + source = env.audio_features.get("provenance", {}).get("sourceSha256") + if source: + metadata["trainingSourceSha256"] = sorted(set(metadata.get("trainingSourceSha256", [])) | {source}) + save_checkpoint(policy, save_path, { + **metadata, "algorithm": "ppo", "seed": seed, + "lastRunTimesteps": timesteps_done, + "features": env.audio_features.get("provenance", {}), + }) print(f"Saved trained PPO policy to {save_path}", flush=True) return { - "final_mean_reward": float(np.mean(ep_rewards) if ep_rewards else current_ep_reward), + "final_mean_reward": float(np.mean(completed_returns) if completed_returns else current_ep_reward), + "timesteps": timesteps_done, + "completed_episodes": len(completed_returns), "policy_loss": avg_p_loss, "value_loss": avg_v_loss, } @@ -244,18 +276,28 @@ def main() -> int: parser.add_argument("--timesteps", type=int, default=1024) parser.add_argument("--rollout-steps", type=int, default=256) parser.add_argument("--epochs", type=int, default=4) - parser.add_argument("--resume", action="store_true", default=True, help="Resume from existing checkpoint if available") + parser.add_argument("--resume", action=argparse.BooleanOptionalAction, default=True) + parser.add_argument("--lr", type=float, default=3e-4) + parser.add_argument("--seed", type=int, default=0) + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument("--analysis-dir", type=Path, help="Verified analyzer artifact directory") + source.add_argument("--synthetic", action="store_true", help="Explicit smoke-test pulse, not real-song training") parser.add_argument("--out", type=Path, default=Path("data/models/ppo_policy.pt")) args = parser.parse_args() - env = BeatSaberEnv(difficulty="Expert") + torch.manual_seed(args.seed) + if args.analysis_dir: + bundle = load_analysis_features(args.analysis_dir) + env = BeatSaberEnv(bundle.audio_features, bundle.beat_grid, bundle.bpm, difficulty="Expert") + else: + env = BeatSaberEnv(difficulty="Expert") policy = ActorCriticPolicy() + prior_metadata = {} if args.resume and args.out.is_file(): - try: - policy.load_state_dict(torch.load(args.out, map_location="cpu")) - print(f"Resumed policy weights from {args.out}", flush=True) - except Exception as e: - print(f"Could not load checkpoint: {e}, starting from scratch", flush=True) + policy, prior_metadata = load_checkpoint(args.out) + if args.analysis_dir and not prior_metadata.get("trainingSourceSha256"): + parser.error("Checkpoint training lineage is unknown. Use --no-resume to start a traceable run.") + print(f"Resumed policy weights from {args.out}", flush=True) train_ppo( policy=policy, @@ -265,6 +307,8 @@ def main() -> int: ppo_epochs=args.epochs, lr=args.lr, save_path=args.out, + seed=args.seed, + checkpoint_metadata=prior_metadata, ) return 0 diff --git a/skills/beat-saber-mapping/scripts/timing_export.py b/skills/beat-saber-mapping/scripts/timing_export.py new file mode 100644 index 0000000..c0c180d --- /dev/null +++ b/skills/beat-saber-mapping/scripts/timing_export.py @@ -0,0 +1,83 @@ +"""Project authored musical beats into the constant BPM clock used by exported maps.""" + +from __future__ import annotations + +from bisect import bisect_right +from copy import deepcopy +import math +from typing import Any + + +class ExportClock: + def __init__(self, analysis: dict[str, Any]): + self.bpm = float(analysis["bpm"]) + self.sample_rate = int(analysis.get("sampleRate", 44100)) + rows = analysis.get("beatGrid", []) + if len(rows) < 2: + raise ValueError("Export timing requires at least two adopted beat/sample rows") + self.beats = [float(row["beat"]) for row in rows] + self.samples = [float(row["sample"]) for row in rows] + if not math.isfinite(self.bpm) or self.bpm <= 0 or self.sample_rate <= 0: + raise ValueError("Invalid export BPM or sample rate") + if not all(math.isfinite(value) for value in self.beats + self.samples): + raise ValueError("Export timing grid contains a non-finite position") + if any(right <= left for values in (self.beats, self.samples) for left, right in zip(values, values[1:])): + raise ValueError("Export timing grid must increase in beat and sample positions") + + def beat(self, authored: float) -> float: + index = min(len(self.beats) - 2, max(0, bisect_right(self.beats, authored) - 1)) + ratio = (authored - self.beats[index]) / (self.beats[index + 1] - self.beats[index]) + sample = self.samples[index] + ratio * (self.samples[index + 1] - self.samples[index]) + # Round to the source sample clock before writing fractional export beats. + return round(round(sample) / self.sample_rate * self.bpm / 60.0, 8) + + +def project_map_timing(beatmap: dict[str, Any], analysis: dict[str, Any]) -> dict[str, Any]: + """Keep absolute and relative generated v3 event times on the analyzed sample clock.""" + clock = ExportClock(analysis) + result = deepcopy(beatmap) + if result.get("customData", {}).get("_beatforgeTiming"): + raise ValueError("Map timing has already been projected") + if result.get("bpmEvents"): + raise ValueError("Authored maps with BPM events require an explicit BPM-event conversion") + for key in ("colorNotes", "bombNotes", "sliders", "burstSliders", "obstacles", "rotationEvents", "basicBeatmapEvents", "colorBoostBeatmapEvents"): + for item in result.get(key, []): + start = float(item["b"]) + item["b"] = clock.beat(start) + if key in {"sliders", "burstSliders"}: + item["tb"] = clock.beat(float(item["tb"])) + elif key == "obstacles": + item["d"] = round(clock.beat(start + float(item["d"])) - item["b"], 8) + if key in {"basicBeatmapEvents", "colorBoostBeatmapEvents"}: + item["b"] = max(0.0, item["b"]) + for key in ("lightColorEventBoxGroups", "lightRotationEventBoxGroups", "lightTranslationEventBoxGroups"): + for group in result.get(key, []): + start = float(group["b"]) + exported_start = clock.beat(start) + group["b"] = max(0.0, exported_start) + for box in group.get("e", []): + # Only event arrays contain relative beat times. The filter's b is a flag. + for array in ("e", "l", "t"): + for event in box.get(array, []): + if "b" in event: + event["b"] = round(clock.beat(start + float(event["b"])) - exported_start, 8) + result["bpmEvents"] = [] + result.setdefault("customData", {})["_beatforgeTiming"] = { + "schemaVersion": 1, "clock": "constant-bpm-sample-projection", + "sampleRate": clock.sample_rate, "bpm": clock.bpm, + } + return result + + +def project_plan_timing(plan: dict[str, Any], analysis: dict[str, Any]) -> dict[str, Any]: + clock = ExportClock(analysis) + result = deepcopy(plan) + for section in result.get("sections", []): + for key in ("startBeat", "endBeat"): + if key in section: + section["authored" + key[0].upper() + key[1:]] = section[key] + section[key] = clock.beat(float(section[key])) + section["startSeconds"] = float(section["startBeat"]) * 60.0 / clock.bpm + section["endSeconds"] = float(section["endBeat"]) * 60.0 / clock.bpm + result["exportClock"] = "constant-bpm-sample-projection" + return result diff --git a/skills/beat-saber-mapping/tests/test_rl_environment.py b/skills/beat-saber-mapping/tests/test_rl_environment.py index 91d5d32..4822f82 100644 --- a/skills/beat-saber-mapping/tests/test_rl_environment.py +++ b/skills/beat-saber-mapping/tests/test_rl_environment.py @@ -179,7 +179,7 @@ def test_ppo_training_loop(): def test_rl_map_generator_v3(): - generator = RLMapGenerator() + generator = RLMapGenerator(policy=ActorCriticPolicy()) bpm = 128.0 grid = [round(i * 0.25, 4) for i in range(32 * 4)] # 32 beats audio = { diff --git a/skills/beat-saber-mapping/tests/test_rl_features.py b/skills/beat-saber-mapping/tests/test_rl_features.py new file mode 100644 index 0000000..b795c91 --- /dev/null +++ b/skills/beat-saber-mapping/tests/test_rl_features.py @@ -0,0 +1,115 @@ +"""Regression checks for real analysis clocks and truthful RL inputs.""" + +import json + +import numpy as np +import pytest + +from rl.features import build_analysis_features, load_analysis_features, STEMS +from rl.library import load_library + + +def documents(): + # Offset is half a second; tempo changes from 120 to 60 at beat 1. + rows = [{"beat": beat, "sample": sample} for beat, sample in + [(0, 22050), (.5, 33075), (1, 44100), (1.5, 66150), (2, 88200)]] + analysis = {"status": "timing_verified", "sampleRate": 44100, "durationSamples": 110250, + "bpm": 120, "sourceSha256": "a" * 64, + "events": [{"sample": 44100, "stemEnergy": {"drums": .8, "bass": .2}}]} + grid = {"sampleRate": 44100, "status": "timing_verified", "beats": rows, "source": "confirmed"} + sections = {"source": "all-in-one", "sections": [ + {"label": "verse", "startSeconds": 0, "endSeconds": 1, "startBeat": 0, "endBeat": 99}, + {"label": "chorus", "startSeconds": 1, "endSeconds": 2.5, "startBeat": 99, "endBeat": 100}, + ]} + frames = {"times": np.arange(11) * .25, "combinedOnset": np.arange(11) / 10, + "flux": np.linspace(1, 0, 11)} + return analysis, grid, sections, frames + + +def write_analysis(path, *, source="a" * 64): + path.mkdir(parents=True, exist_ok=True) + analysis, grid, sections, frames = documents() + analysis["sourceSha256"] = source + for name, doc in [("analysis.json", analysis), ("beat_grid.json", grid), ("sections.json", sections)]: + (path / name).write_text(json.dumps(doc), encoding="utf-8") + np.savez_compressed(path / "audio_features.npz", **frames) + return path + + +def test_offset_and_tempo_changes_use_actual_sample_positions(): + result = build_analysis_features(*documents()) + assert result.beat_grid == [i / 4 for i in range(9)] + assert result.audio_features["samplePositions"] == [22050, 27562, 33075, 38588, 44100, 55125, 66150, 77175, 88200] + assert result.audio_features["onsets"][0] == pytest.approx(.2) + assert result.audio_features["onsets"][-1] == pytest.approx(.8) + assert result.audio_features["stepBpms"][:4] == pytest.approx([120] * 4) + assert result.audio_features["stepBpms"][4:] == pytest.approx([60] * 5) + assert result.provenance["durationSamples"] == 110250 + + +def test_sections_use_seconds_over_stale_constant_tempo_beat_fields(): + result = build_analysis_features(*documents()) + assert result.audio_features["sections"] == ["verse"] * 4 + ["chorus"] * 5 + + +def test_stems_are_real_localized_annotations_and_missing_stems_are_honest(): + result = build_analysis_features(*documents()) + audio = result.audio_features + assert audio["stems"]["drums"] == [0, 0, 0, 0, .8, 0, 0, 0, 0] + assert audio["stemAvailability"]["drums"] is True + assert audio["stemAvailability"]["vocals"] is False + assert audio["stems"]["vocals"] == [0] * 9 + analysis, grid, sections, frames = documents() + analysis["events"] = [] + missing = build_analysis_features(analysis, grid, sections, frames) + assert all(not missing.audio_features["stemAvailability"][stem] for stem in STEMS) + assert all(not any(values) for values in missing.audio_features["stems"].values()) + + +def test_hop_clock_does_not_stretch_frames_to_grid_length(): + analysis, grid, sections, _ = documents() + frames = {"hopSamples": 11025, "combinedOnset": np.arange(11) / 10} + result = build_analysis_features(analysis, grid, sections, frames) + assert result.audio_features["onsets"][0] == pytest.approx(.2) + assert result.audio_features["onsets"][-1] == pytest.approx(.8) + assert result.audio_features["flux"] == [0] * 9 + assert result.provenance["fluxAvailable"] is False + + +@pytest.mark.parametrize("bad", [float("nan"), 33074.5, 22050]) +def test_invalid_sample_grid_fails(bad): + args = list(documents()) + args[1]["beats"][1]["sample"] = bad + with pytest.raises(ValueError, match="strictly increasing"): + build_analysis_features(*args) + + +def test_unconfirmed_timing_requires_explicit_generation_override(): + analysis, grid, sections, frames = documents() + analysis["status"] = grid["status"] = "needs_anchors" + with pytest.raises(ValueError, match="verified"): + build_analysis_features(analysis, grid, sections, frames) + allowed = build_analysis_features(analysis, grid, sections, frames, require_verified=False) + assert allowed.provenance["timingStatus"] == "needs_anchors" + + +def test_artifact_hashes_and_repeatability(tmp_path): + path = write_analysis(tmp_path / "analysis") + first = load_analysis_features(path) + assert first == load_analysis_features(path) + assert len(first.provenance["artifactSha256"]) == 4 + (path / "audio_features.npz").unlink() + with pytest.raises(FileNotFoundError, match="Run analyze_audio"): + load_analysis_features(path) + + +def test_library_rejects_content_overlap_even_if_track_names_differ(tmp_path): + write_analysis(tmp_path / "one") + write_analysis(tmp_path / "two") + manifest = tmp_path / "library.json" + manifest.write_text(json.dumps({"schemaVersion": 1, "tracks": [ + {"id": "first", "analysisDir": "one", "split": "train"}, + {"id": "different-name", "analysisDir": "two", "split": "validation"}, + ]})) + with pytest.raises(ValueError, match="split leakage"): + load_library(manifest) diff --git a/skills/beat-saber-mapping/tests/test_rl_invariants.py b/skills/beat-saber-mapping/tests/test_rl_invariants.py new file mode 100644 index 0000000..2ef21c1 --- /dev/null +++ b/skills/beat-saber-mapping/tests/test_rl_invariants.py @@ -0,0 +1,113 @@ +"""Independent serialization, episode-boundary, and reproducibility regressions.""" + +from collections import Counter + +import numpy as np +import pytest + +torch = pytest.importorskip("torch") + +from beatforge_core import ValidationReport +from rl.checkpoints import save_checkpoint +from rl.environment import BeatSaberEnv +from rl.models import ActorCriticPolicy +from rl.policy_generator import RLMapGenerator +from rl.safety import safe_hold_candidates +from rl.train_ppo import RolloutBuffer, train_ppo +from timing_export import project_map_timing +from validate_map import validate_v3 + + +def fixture_audio(length=96, bpm=160, variable=False): + grid = [index / 4 for index in range(length)] + tempos = [90 if index < length // 2 else 220 for index in range(length)] if variable else [bpm] * length + samples = [11025] + for index in range(length - 1): + samples.append(samples[-1] + round(44100 * 60 / tempos[index] / 4)) + audio = { + "onsets": [.95] * length, "flux": [.8] * length, "sustainBeats": [2.] * length, + "sections": ["chorus"] * length, + "stems": {name: [.9] * length for name in ("drums", "bass", "guitar", "piano", "vocals", "other")}, + "timesSeconds": [sample / 44100 for sample in samples], "stepBpms": tempos, + } + analysis = {"bpm": bpm, "sampleRate": 44100, + "beatGrid": [{"beat": beat, "sample": sample} for beat, sample in zip(grid, samples)]} + return audio, grid, analysis + + +@pytest.mark.parametrize("difficulty", ["Easy", "Hard", "ExpertPlus"]) +@pytest.mark.parametrize("bpm,variable", [(90, False), (220, False), (120, True)]) +@pytest.mark.parametrize("seed", [0, 3]) +def test_dense_adversarial_rl_output_passes_independent_exported_validator(difficulty, bpm, variable, seed): + torch.manual_seed(seed) + audio, grid, analysis = fixture_audio(bpm=bpm, variable=variable) + chart = RLMapGenerator(policy=ActorCriticPolicy(hidden_dim=16)).generate_difficulty( + audio, grid, bpm, difficulty=difficulty, deterministic=False, seed=seed, + ) + report = ValidationReport() + validate_v3(project_map_timing(chart, analysis), "fixture.dat", report, bpm=bpm, difficulty=difficulty) + assert len(chart["colorNotes"]) > len(grid) // 8, "Passing by emitting an empty or nearly empty chart is not acceptable" + assert not report.errors, dict(Counter(issue.code for issue in report.errors)) + + +@pytest.mark.parametrize("deterministic", [True, False]) +def test_checkpoint_generation_is_reproducible_and_preserves_caller_rng(tmp_path, deterministic): + torch.manual_seed(17) + checkpoint = tmp_path / "policy.pt" + save_checkpoint(ActorCriticPolicy(hidden_dim=16), checkpoint, {"trainingSourceSha256": ["a" * 64]}) + audio, grid, _ = fixture_audio(length=40) + state = torch.random.get_rng_state().clone() + first = RLMapGenerator(model_path=checkpoint).generate_difficulty(audio, grid, 160, deterministic=deterministic, seed=42) + assert torch.equal(state, torch.random.get_rng_state()) + torch.rand(13) # Unrelated application use must not change the map's random stream. + second = RLMapGenerator(model_path=checkpoint).generate_difficulty(audio, grid, 160, deterministic=deterministic, seed=42) + assert first == second + if not deterministic: + other = RLMapGenerator(model_path=checkpoint).generate_difficulty(audio, grid, 160, deterministic=False, seed=43) + assert other["colorNotes"] != first["colorNotes"] + + +def test_safe_holds_survive_without_erasing_notes_or_overlapping_ownership(): + notes = [{"b": beat, "c": 0, "x": 1, "y": 0, "d": direction} for beat, direction in [(0, 1), (1, 0), (2, 1), (4, 0)]] + arc = {"b": 0, "c": 0, "x": 1, "y": 0, "d": 1, "tb": 1, "tx": 1, "ty": 0, "tc": 0, "mu": 1, "tmu": 1, "m": 0} + chain = {"b": 2, "c": 0, "x": 1, "y": 0, "d": 1, "tb": 2.25, "tx": 1, "ty": 0, "sc": 5, "s": 1} + conflicting = {**chain, "b": 0, "tb": .25} + grid = [index / 4 for index in range(17)] + arcs, chains, rejected = safe_hold_candidates(notes, [arc], [chain, conflicting], {}, grid, 120, "Easy") + assert arcs == [arc] and chains == [chain] + assert rejected["hold_occupancy"] == 1 + report = ValidationReport() + validate_v3({"version": "3.3.0", "colorNotes": notes, "sliders": arcs, "burstSliders": chains}, + "fixture.dat", report, bpm=120, difficulty="Easy") + assert not report.errors + assert len(notes) == 4 + + +@pytest.mark.parametrize("terminal,expected", [(True, 2.), (False, 8.3)]) +def test_actual_ppo_rollout_distinguishes_terminal_and_time_limit(monkeypatch, terminal, expected): + class BoundaryEnv(BeatSaberEnv): + def reset(self, **kwargs): + super().reset(**kwargs) + return np.zeros(self.obs_dim, dtype=np.float32), {} + + def step(self, action): + return np.ones(self.obs_dim, dtype=np.float32), 2., terminal, not terminal, {} + + policy = ActorCriticPolicy(hidden_dim=16) + # Final observation has value 7; reset observation has deliberately huge value + # 123. Neither a true terminal nor the new episode may bootstrap from 123. + monkeypatch.setattr(policy, "forward", lambda obs: (None, None, torch.where(obs[:, 0] > 0, 7., 123.))) + recorded = {} + original = RolloutBuffer.compute_gae + + def inspect(self, *args, **kwargs): + original(self, *args, **kwargs) + recorded["return"] = self.returns[0].item() + recorded["reward"] = self.rewards[0].item() + recorded["done"] = self.dones[0].item() + + monkeypatch.setattr(RolloutBuffer, "compute_gae", inspect) + result = train_ppo(policy, BoundaryEnv(beat_grid=[0, .25]), total_timesteps=1, + rollout_steps=1, ppo_epochs=1, batch_size=1, gamma=.9, seed=4) + assert recorded == pytest.approx({"return": expected, "reward": expected, "done": 1.}) + assert result["final_mean_reward"] == 2., "Reported episode reward must exclude the bootstrap value" diff --git a/skills/beat-saber-mapping/tests/test_rl_registry.py b/skills/beat-saber-mapping/tests/test_rl_registry.py new file mode 100644 index 0000000..6171bae --- /dev/null +++ b/skills/beat-saber-mapping/tests/test_rl_registry.py @@ -0,0 +1,122 @@ +"""Promotion requires the same benchmark, measured progress, and no regressions.""" + +import copy +import json + +import pytest + +from rl.features import FEATURE_SCHEMA +from rl.registry import active_checkpoint, compare_evaluations, register_checkpoint, rollback, registry_status + + +def evaluation(checkpoint_hash, onset=.5): + return { + "schemaVersion": 1, "kind": "offline-held-out-checkpoint-evaluation", + "featureSchema": FEATURE_SCHEMA, "heldOutVerifiedAgainstRecordedTraining": True, + "split": "validation", "difficulty": "Expert", "seed": 0, "deterministic": True, + "manifestSha256": "d" * 64, "runtime": {"torch": "test", "numpy": "test"}, + "evaluatorSha256": {"evaluate.py": "e" * 64}, + "results": [{"checkpoint": {"checkpointSha256": checkpoint_hash, "trainingSourceSha256": ["a" * 64]}, + "tracks": [{"id": "held-out", "features": { + "sourceSha256": "b" * 64, "timingStatus": "timing_verified", + "artifactSha256": {"beat_grid.json": "c" * 64}, + }, "metrics": {"hardErrors": 0, "notes": 100, "nps": 4.25, + "handImbalance": .1, "mostRepeatedTransitionFraction": .1, + "warnings": 0, "meanOnsetStrengthAtNotes": onset}}]}], + } + + +def test_promotion_requires_primary_gain_and_preserves_other_metrics(): + result = compare_evaluations(evaluation("before"), "before", evaluation("after", .6), "after") + assert result["meanGain"] == pytest.approx(.1) + assert result["humanPreferenceEstablished"] is False + + +@pytest.mark.parametrize("key,value", [ + ("hardErrors", 1), ("notes", 0), ("handImbalance", .2), + ("mostRepeatedTransitionFraction", .3), ("warnings", 1), ("nps", 7), + ("meanOnsetStrengthAtNotes", .5), ("meanOnsetStrengthAtNotes", float("nan")), +]) +def test_regression_or_no_improvement_cannot_promote(key, value): + candidate = evaluation("after", .7) + candidate["results"][0]["tracks"][0]["metrics"][key] = value + with pytest.raises(ValueError): + compare_evaluations(evaluation("before"), "before", candidate, "after") + + +@pytest.mark.parametrize("field,value", [("manifestSha256", "different"), ("seed", 4), + ("difficulty", "Hard"), ("evaluatorSha256", {})]) +def test_changed_benchmark_cannot_promote(field, value): + candidate = evaluation("after", .7) + candidate[field] = value + with pytest.raises(ValueError, match="Benchmark identity"): + compare_evaluations(evaluation("before"), "before", candidate, "after") + + +def test_one_improved_track_cannot_hide_a_regression_elsewhere(): + baseline = evaluation("before") + second = copy.deepcopy(baseline["results"][0]["tracks"][0]) + second["id"] = "second" + second["features"]["sourceSha256"] = "f" * 64 + baseline["results"][0]["tracks"].append(second) + candidate = copy.deepcopy(baseline) + candidate["results"][0]["checkpoint"]["checkpointSha256"] = "after" + candidate["results"][0]["tracks"][0]["metrics"]["meanOnsetStrengthAtNotes"] = 1 + candidate["results"][0]["tracks"][1]["metrics"]["meanOnsetStrengthAtNotes"] = .4 + with pytest.raises(ValueError, match="Regression in onset alignment"): + compare_evaluations(baseline, "before", candidate, "after") + + +def test_registry_promotion_and_rollback_preserve_checkpoint_files(tmp_path): + torch = pytest.importorskip("torch") + from rl.checkpoints import load_checkpoint, save_checkpoint + from rl.models import ActorCriticPolicy + paths = [] + for seed in (1, 2, 3): + torch.manual_seed(seed) + path = tmp_path / f"policy-{seed}.pt" + save_checkpoint(ActorCriticPolicy(hidden_dim=16), path, {"trainingSourceSha256": ["a" * 64]}) + _, metadata = load_checkpoint(path) + report_path = tmp_path / f"report-{seed}.json" + report_path.write_text(json.dumps(evaluation(metadata["checkpointSha256"], .4 + seed / 10))) + paths.append((path, report_path)) + registry = tmp_path / "registry" + baseline = register_checkpoint(registry, *paths[0], initialize=True) + previous_bytes = active_checkpoint(registry).read_bytes() + promoted = register_checkpoint(registry, *paths[1]) + assert promoted["previous"] == baseline["active"] + assert active_checkpoint(registry).read_bytes() == paths[1][0].read_bytes() + status = registry_status(registry) + assert status["status"] == "ready" + assert status["modelCount"] == 2 + assert status["automaticTraining"] is False + assert (registry / "checkpoints" / f"{baseline['active']}.pt").read_bytes() == previous_bytes + reverted = rollback(registry) + assert reverted["active"] == baseline["active"] + assert active_checkpoint(registry).read_bytes() == previous_bytes + assert len(list((registry / "checkpoints").glob("*.pt"))) == 2 + # A rejected promotion must not change active registry state. + bad = evaluation(load_checkpoint(paths[2][0])[1]["checkpointSha256"], .9) + bad["results"][0]["tracks"][0]["metrics"]["hardErrors"] = 1 + paths[2][1].write_text(json.dumps(bad)) + state_before = (registry / "registry.json").read_bytes() + with pytest.raises(ValueError, match="hard validation"): + register_checkpoint(registry, *paths[2]) + assert (registry / "registry.json").read_bytes() == state_before + + +def test_registry_rejects_tampered_active_artifact(tmp_path): + registry = tmp_path / "registry" + (registry / "checkpoints").mkdir(parents=True) + digest = "a" * 64 + (registry / "registry.json").write_text(json.dumps({"active": digest})) + (registry / "checkpoints" / f"{digest}.pt").write_bytes(b"wrong content") + with pytest.raises(ValueError, match="content hash"): + active_checkpoint(registry) + assert registry_status(registry)["status"] == "invalid" + + +def test_uninitialized_registry_has_honest_status(tmp_path): + assert registry_status(tmp_path / "missing") == { + "status": "empty", "active": None, "modelCount": 0, "automaticTraining": False, + } diff --git a/skills/beat-saber-mapping/tests/test_rl_training_correctness.py b/skills/beat-saber-mapping/tests/test_rl_training_correctness.py new file mode 100644 index 0000000..df361ee --- /dev/null +++ b/skills/beat-saber-mapping/tests/test_rl_training_correctness.py @@ -0,0 +1,167 @@ +"""Small deterministic RL regressions; no downloads or long training run.""" + +import json + +import numpy as np +import pytest + +torch = pytest.importorskip("torch") + +from rl.checkpoints import load_checkpoint, save_checkpoint +from rl.environment import BeatSaberEnv, decode_hand_action +from rl.evaluate import chart_metrics, evaluate_checkpoints +from rl.features import load_analysis_features +from rl.models import ActorCriticPolicy +from rl.policy_generator import RLMapGenerator +from rl.train_custom_tracks import extract_track_features, train_on_custom_library +from rl.train_ppo import RolloutBuffer, train_ppo +from test_rl_features import write_analysis +from kinematics import cross_hand_finding, vision_blocking_double + + +def test_terminal_last_slot_does_not_bootstrap_reset_episode_value(): + buffer = RolloutBuffer(1, 69) + buffer.ptr = 1 + buffer.rewards[0] = 3 + buffer.values[0] = 2 + buffer.dones[0] = 1 + buffer.compute_gae(last_value=100, gamma=.9) + assert buffer.returns[0].item() == pytest.approx(3) + assert torch.isfinite(buffer.advantages).all() + + +def test_gae_cuts_internal_episode_boundary_but_bootstraps_nonterminal_tail(): + buffer = RolloutBuffer(3, 69) + buffer.ptr = 3 + buffer.rewards[:] = torch.tensor([1, 2, 5]) + buffer.values[:] = torch.tensor([0, 0, 0]) + buffer.dones[:] = torch.tensor([0, 1, 0]) + buffer.compute_gae(last_value=10, gamma=.5, gae_lambda=1) + assert buffer.returns.tolist() == pytest.approx([2, 2, 10]) + + +def test_inference_requires_checkpoint_and_rejects_corrupt_weights(tmp_path): + with pytest.raises(ValueError, match="explicit trained checkpoint"): + RLMapGenerator() + with pytest.raises(FileNotFoundError): + RLMapGenerator(model_path=tmp_path / "missing.pt") + path = tmp_path / "broken.pt" + path.write_bytes(b"not a checkpoint") + with pytest.raises(ValueError, match="Invalid RL checkpoint"): + RLMapGenerator(model_path=path) + + +def test_checkpoint_hash_and_feature_provenance(tmp_path): + policy = ActorCriticPolicy(hidden_dim=32) + path = tmp_path / "policy.pt" + save_checkpoint(policy, path, {"seed": 4, "trainingSourceSha256": ["a" * 64]}) + loaded, metadata = load_checkpoint(path) + assert metadata["seed"] == 4 + assert len(metadata["checkpointSha256"]) == 64 + assert all(torch.equal(tensor, loaded.state_dict()[key]) for key, tensor in policy.state_dict().items()) + bad = policy.state_dict() + bad["encoder.0.weight"][0, 0] = float("nan") + torch.save(bad, path) + with pytest.raises(ValueError, match="non-finite"): + load_checkpoint(path) + + +def test_training_and_inference_load_identical_features(tmp_path): + path = write_analysis(tmp_path / "analysis") + audio, grid, bpm = extract_track_features(path) + bundle = load_analysis_features(path) + assert audio == bundle.audio_features + assert grid == bundle.beat_grid + assert bpm == bundle.bpm + assert audio["sections"][-1] == "chorus" + + +def test_short_ppo_run_stops_at_requested_steps_and_repeats_with_seed(): + states = [] + for _ in range(2): + torch.manual_seed(23) + policy = ActorCriticPolicy(hidden_dim=16) + env = BeatSaberEnv(beat_grid=[0, .25]) + metrics = train_ppo(policy, env, total_timesteps=3, rollout_steps=2, ppo_epochs=1, batch_size=2, seed=23) + assert metrics["timesteps"] == 3 + assert metrics["completed_episodes"] == 1 + assert np.isfinite(metrics["policy_loss"]) + states.append({key: value.clone() for key, value in policy.state_dict().items()}) + assert all(torch.equal(states[0][key], states[1][key]) for key in states[0]) + + +def test_environment_seed_does_not_mutate_numpy_global_rng(): + np.random.seed(321) + expected = np.random.random() + np.random.seed(321) + env = BeatSaberEnv(beat_grid=[0, .25]) + env.reset(seed=77) + assert np.random.random() == expected + assert not any(env.audio_features["stemAvailability"].values()) + + +def test_blue_mask_rejects_every_unsafe_simultaneous_pair(): + env = BeatSaberEnv(beat_grid=[0, .25]) + for red_action in range(1, env.action_dim): + red_pose = decode_hand_action(red_action) + red = {"b": 0, "c": 0, "x": red_pose[0], "y": red_pose[1], "d": red_pose[2]} + mask = env.blue_action_mask(red_action) + assert mask[0] + for blue_action in np.flatnonzero(mask[1:]) + 1: + x, y, direction = decode_hand_action(int(blue_action)) + blue = {"b": 0, "c": 1, "x": x, "y": y, "d": direction} + assert cross_hand_finding(red, blue) is None + assert not vision_blocking_double(red, blue) + + +def test_conditional_sampling_and_ppo_replay_have_identical_log_probabilities(): + torch.manual_seed(42) + env = BeatSaberEnv(beat_grid=[0, .25]) + policy = ActorCriticPolicy(hidden_dim=16) + obs = torch.as_tensor(env.reset()[0]).unsqueeze(0) + mask_red, mask_blue = env.action_masks() + conditional = lambda chosen: torch.as_tensor(env.blue_action_mask(int(chosen.item()), mask_blue)).unsqueeze(0) + action, sampled_log_prob, _, _ = policy.get_action_and_value( + obs, mask_red=torch.as_tensor(mask_red).unsqueeze(0), + mask_blue=torch.as_tensor(mask_blue).unsqueeze(0), blue_mask_fn=conditional, + ) + effective_blue = conditional(action[0]) + assert effective_blue[0, int(action[1].item())] + _, replay_log_prob, _, _ = policy.get_action_and_value( + obs, action=action, mask_red=torch.as_tensor(mask_red).unsqueeze(0), mask_blue=effective_blue, + ) + assert sampled_log_prob.item() == pytest.approx(replay_log_prob.item()) + + +def test_benchmark_uses_held_out_hashes_and_reports_real_validation(tmp_path): + write_analysis(tmp_path / "train", source="a" * 64) + write_analysis(tmp_path / "held-out", source="b" * 64) + manifest = tmp_path / "library.json" + manifest.write_text(json.dumps({"schemaVersion": 1, "tracks": [ + {"id": "train", "analysisDir": "train", "split": "train"}, + {"id": "held-out", "analysisDir": "held-out", "split": "validation"}, + ]})) + policy = ActorCriticPolicy(hidden_dim=16) + for parameter in policy.parameters(): + parameter.data.zero_() # Deterministic idle policy, only for the fixture. + path = tmp_path / "policy.pt" + save_checkpoint(policy, path, {"trainingSourceSha256": ["a" * 64]}) + result = evaluate_checkpoints(manifest, [path], seed=3) + assert result["heldOutVerifiedAgainstRecordedTraining"] + assert result["results"][0]["tracks"][0]["id"] == "held-out" + assert result["results"][0]["tracks"][0]["metrics"]["notes"] == 0 + assert result["results"][0]["tracks"][0]["metrics"]["headsetPlaytest"] == "not_performed" + save_checkpoint(policy, path, {"trainingSourceSha256": ["b" * 64]}) + with pytest.raises(ValueError, match="used to train"): + evaluate_checkpoints(manifest, [path]) + + +def test_structural_benchmark_detects_colliding_notes(tmp_path): + bundle = load_analysis_features(write_analysis(tmp_path / "analysis")) + chart = {"version": "3.3.0", "colorNotes": [ + {"b": 1, "c": 0, "x": 1, "y": 0, "d": 1}, + {"b": 1, "c": 1, "x": 1, "y": 0, "d": 1}, + ]} + result = chart_metrics(chart, bundle, "Expert") + assert result["hardErrors"] > 0 + assert result["nps"] == pytest.approx(.8) diff --git a/src/beatforge/api.py b/src/beatforge/api.py index ab97657..0298ca8 100644 --- a/src/beatforge/api.py +++ b/src/beatforge/api.py @@ -3,7 +3,10 @@ from __future__ import annotations import json +import math import shutil +import sys +import threading import time import traceback import uuid @@ -17,7 +20,14 @@ from pydantic import BaseModel, Field from beatforge.install import find_custom_levels, install_generated_map_elevated, install_map, launch_beat_saber -from beatforge.premium import ROOT, SKILL, package_map, run_premium_pipeline, summarize_map +from beatforge.premium import ROOT, SKILL, PipelineCancelled, package_map, run_premium_pipeline, summarize_map +from beatforge.mapping_plan import normalize_mapping_plan + +# The RL registry is a portable skill module rather than an installed package. +# Make its import deterministic for a first request made before /api/learning. +_SKILL_SCRIPTS = SKILL / "scripts" +if str(_SKILL_SCRIPTS) not in sys.path: + sys.path.insert(0, str(_SKILL_SCRIPTS)) from beatforge.release_route import ai_release_route_enabled from beatforge.providers import ( PROVIDERS, @@ -39,7 +49,8 @@ IMPORTS_DIR = ROOT / "data" / "imports" SETTINGS_FILE = ROOT / "data" / "provider-settings.json" METADATA_DIR = ROOT / "data" / "metadata" -JOB_TTL_SECONDS = 24 * 3600 +ACTIVE_STATES = {"queued", "running", "validating", "cancelling"} +STATUS_LOCK = threading.RLock() STUDIO_STATES = ( "needs_anchors", "needs_palette", @@ -51,6 +62,8 @@ "release_candidate", "invalid", "error", + "interrupted", + "cancelled", ) TERMINAL_STATES = {state for state in STUDIO_STATES if state != "validating"} STUDIO_DIFFICULTIES = ("Easy", "Normal", "Hard", "Expert", "ExpertPlus") @@ -207,21 +220,26 @@ def _installed_playtest_maps() -> list[dict[str, Any]]: return maps -def _cleanup_old_jobs(max_age: float = JOB_TTL_SECONDS) -> int: +def _reconcile_interrupted_jobs() -> int: + """Keep saved work and make interrupted process-local workers retryable.""" if not JOBS_DIR.is_dir(): return 0 - now = time.time() - removed = 0 + recovered = 0 for directory in JOBS_DIR.iterdir(): if not directory.is_dir(): continue try: - if now - directory.stat().st_mtime > max_age: - shutil.rmtree(directory, ignore_errors=True) - removed += 1 - except OSError: + status = _read_status(directory.name) + if status.get("status") not in ACTIVE_STATES: + continue + cancelled = (directory / "cancel.requested").is_file() + status.update(status="cancelled" if cancelled else "interrupted", localStatus=None) + status = _append_stage(status, status["status"], "Studio restarted before this run finished. Retry uses the saved audio, anchors, palette, and mapping settings.") + _write_status(directory.name, status) + recovered += 1 + except (OSError, ValueError, HTTPException): continue - return removed + return recovered def _metadata_manifest_path(metadata_id: str) -> Path: @@ -265,7 +283,7 @@ def _metadata_cover_path(metadata_id: str | None) -> Path | None: @app.on_event("startup") def _on_startup() -> None: - _cleanup_old_jobs() + _reconcile_interrupted_jobs() def _job_dir(job_id: str) -> Path: @@ -284,9 +302,48 @@ def _read_status(job_id: str) -> dict[str, Any]: def _write_status(job_id: str, data: dict[str, Any]) -> None: directory = _job_dir(job_id) directory.mkdir(parents=True, exist_ok=True) - temporary = directory / "status.json.tmp" - temporary.write_text(json.dumps(data, indent=2), encoding="utf-8") - temporary.replace(directory / "status.json") + with STATUS_LOCK: + timestamp = time.time() + data = {**data, "id": job_id, "createdAt": data.get("createdAt", data.get("startedAt", timestamp)), "updatedAt": timestamp} + _write_json(directory / "status.json", data) + + +def _write_json(path: Path, data: Any) -> None: + temporary = path.with_name(f"{path.name}.{uuid.uuid4().hex}.tmp") + try: + temporary.write_text(json.dumps(data, indent=2), encoding="utf-8") + temporary.replace(path) + finally: + temporary.unlink(missing_ok=True) + + +def _cancel_requested(job_id: str) -> bool: + return (_job_dir(job_id) / "cancel.requested").is_file() + + +def _qa_report(map_dir: Path) -> dict[str, Any]: + """A missing or malformed report cannot establish that hard gates passed.""" + path = map_dir / "_beatforge" / "qa_report.json" + try: + report = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(report, dict) or not isinstance(report.get("errors"), list): + raise ValueError("QA report must contain an errors list") + if not isinstance(report.get("warnings", []), list): + raise ValueError("QA report warnings must be a list") + if report.get("status") not in {"playtest_candidate", "release_candidate"} and not report["errors"]: + raise ValueError("QA report has not passed validation") + return report + except (OSError, ValueError) as error: + return {"status": "invalid", "errors": [f"QA report unavailable or invalid: {error}"], "warnings": []} + + +def _public_job(data: dict[str, Any]) -> dict[str, Any]: + result = {key: value for key, value in data.items() if key != "trace"} + job_id = str(result["id"]) + active = result.get("status") in ACTIVE_STATES + result["canCancel"] = active and result.get("status") != "cancelling" + result["canRetry"] = not active and not result.get("revisionOf") and any(_job_dir(job_id).glob("input.*")) + return result def _audio_path(job_id: str) -> Path: @@ -303,6 +360,8 @@ def _append_stage(data: dict[str, Any], stage: str, detail: str) -> dict[str, An def _install_pack_to_custom_levels(job_id: str, data: dict[str, Any]) -> Path: + if data.get("localStatus") not in {"playtest_candidate", "unconfirmed_pack"} or _qa_report(_job_dir(job_id) / "map")["errors"]: + raise ValueError("The map must pass its local QA report before installation") zip_path = _job_dir(job_id) / "map.zip" if not zip_path.is_file(): raise FileNotFoundError("Generated map zip is missing") @@ -327,7 +386,7 @@ def progress(stage: str, detail: str, percent: float | None = None, log: bool = current = {**current, "stage": stage, "detail": detail} current.update( { - "status": "validating" if stage == "validating" else "running", + "status": "cancelling" if _cancel_requested(job_id) else "validating" if stage == "validating" else "running", "elapsed": round(time.time() - start, 2), "startedAt": current.get("startedAt") or start, } @@ -337,6 +396,8 @@ def progress(stage: str, detail: str, percent: float | None = None, log: bool = _write_status(job_id, current) try: + if _cancel_requested(job_id): + raise PipelineCancelled("Generation cancelled") job_dir = _job_dir(job_id) map_dir = job_dir / "map" anchors = job_dir / "anchors.json" @@ -353,17 +414,21 @@ def progress(stage: str, detail: str, percent: float | None = None, log: bool = palette=palette if palette.is_file() else None, cover=metadata_cover, progress=progress, - allow_unconfirmed=True, + allow_unconfirmed=bool(initial.get("continueUnconfirmed", False)), difficulties=list(initial.get("difficulties") or STUDIO_DIFFICULTIES), + mapping_plan=initial.get("mappingPlan"), + engine=str(initial.get("engine") or "premium"), + rl_model=Path(initial["rlModel"]) if initial.get("rlModel") else None, + cancel_requested=lambda: _cancel_requested(job_id), ) + if _cancel_requested(job_id): + raise PipelineCancelled("Generation cancelled") status = result["status"] map_ready = (map_dir / "Info.dat").is_file() if map_ready and status in {"playtest_candidate", "invalid"}: progress("validating", "checking schema, timing, same-color flow, collisions, hazards, and packaging") - package_map(map_dir, job_dir / "map.zip") summary = summarize_map(map_dir) - qa_path = map_dir / "_beatforge" / "qa_report.json" - qa = json.loads(qa_path.read_text(encoding="utf-8")) if qa_path.is_file() else {"status": "invalid"} + qa = _qa_report(map_dir) qa_failed = bool(qa.get("errors")) analysis_path = map_dir / "_beatforge" / "analysis.json" timing_verified = False @@ -397,6 +462,13 @@ def progress(stage: str, detail: str, percent: float | None = None, log: bool = "timingVerified": timing_verified, } ) + if qa_failed: + current["error"] = "Local validation failed. Open the QA report before retrying." + _write_status(job_id, current) + return + if _cancel_requested(job_id): + raise PipelineCancelled("Generation cancelled") + package_map(map_dir, job_dir / "map.zip") try: destination = _install_pack_to_custom_levels(job_id, current) except Exception as error: @@ -422,8 +494,12 @@ def progress(stage: str, detail: str, percent: float | None = None, log: bool = if status in {"error", "invalid", "corpus_incomplete"}: current["error"] = (result.get("stderr") or result.get("stdout") or "pipeline failed")[-4000:] _write_status(job_id, current) + except PipelineCancelled: + current = _append_stage(_read_status(job_id), "cancelled", "Generation stopped. Retry keeps the saved audio and mapping settings.") + _write_status(job_id, {**current, "status": "cancelled", "localStatus": None, "elapsed": round(time.time() - start, 2)}) except Exception as error: - _write_status(job_id, {**initial, "status": "error", "error": str(error), "trace": traceback.format_exc(), "elapsed": round(time.time() - start, 2)}) + current = _read_status(job_id) + _write_status(job_id, {**current, "status": "error", "localStatus": None, "error": str(error), "trace": traceback.format_exc(), "elapsed": round(time.time() - start, 2)}) def _settings() -> dict[str, Any]: @@ -460,11 +536,13 @@ def _review_passes(job_dir: Path) -> tuple[bool, list[dict[str, Any]]]: def _playtest_gate(job_dir: Path, required: list[str] | None = None) -> dict[str, Any]: path = job_dir / "playtests.json" evidence = json.loads(path.read_text(encoding="utf-8")) if path.is_file() else [] - passing = [item for item in evidence if item.get("passed")] + latest = {(item.get("difficulty"), item.get("speed"), item.get("tester")): item for item in evidence} + failures = {(item.get("difficulty"), item.get("speed")) for item in latest.values() if not item.get("passed")} + passing = [item for item in latest.values() if item.get("passed") and (item.get("difficulty"), item.get("speed")) not in failures] full = {item.get("difficulty") for item in passing if item.get("speed") == "full"} slow = {item.get("difficulty") for item in passing if item.get("speed") == "slow"} fresh = [item for item in passing if item.get("freshSightRead")] - primary_testers = {item.get("tester") for item in passing if not item.get("freshSightRead")} + primary_testers = {item.get("tester") for item in evidence if not item.get("freshSightRead")} required_difficulties = set(required or STUDIO_DIFFICULTIES) expert_slow = {name for name in ("Expert", "ExpertPlus") if name in required_difficulties} return { @@ -472,6 +550,7 @@ def _playtest_gate(job_dir: Path, required: list[str] | None = None) -> dict[str "expertSlow": expert_slow.issubset(slow), "separateFreshSightRead": any(item.get("tester") not in primary_testers for item in fresh), "evidenceCount": len(evidence), + "unresolvedFailures": len(failures), } @@ -493,11 +572,16 @@ def _sync_playtest_provenance(job_dir: Path, playtests: dict[str, Any]) -> dict[ gate["fullSpeedVrPlaytest"] = bool(playtests.get("allFiveFullSpeed")) gate["slowVrPlaytest"] = bool(playtests.get("expertSlow")) gate["freshSightRead"] = bool(playtests.get("separateFreshSightRead")) - path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + _write_json(path, payload) return gate def _update_release_status(job_id: str) -> dict[str, Any]: + with STATUS_LOCK: + return _update_release_status_locked(job_id) + + +def _update_release_status_locked(job_id: str) -> dict[str, Any]: job_dir = _job_dir(job_id) status = _read_status(job_id) review_passed, reports = _review_passes(job_dir) @@ -598,16 +682,31 @@ async def generate( mapper: str = Form("BeatForge"), seed: int = Form(42), difficulties: str | None = Form(None), - metadata_id: str | None = Form(None), + metadata_id: str | None = Form(None, alias="metadataId"), + mapping_plan: str | None = Form(None, alias="mappingPlan"), + engine: str = Form("premium"), + rl_model: str | None = Form(None, alias="rlModel"), ) -> JSONResponse: suffix = Path(audio.filename or "song.mp3").suffix.casefold() or ".mp3" if suffix not in {".mp3", ".wav", ".ogg", ".egg", ".flac", ".m4a", ".mp4"}: raise HTTPException(400, "Upload MP3, WAV, OGG, FLAC, M4A, or MP4 audio") chosen = parse_studio_difficulties(difficulties) + if engine not in {"premium", "rl"}: + raise HTTPException(400, "Engine must be premium or rl") + if engine == "rl" and not (rl_model or "").strip(): + from beatforge.learning import LEARNING_FILE + from rl.registry import active_checkpoint + try: + rl_model = str(active_checkpoint(LEARNING_FILE.parent / "model_registry").resolve()) + except (OSError, ValueError, KeyError, TypeError) as error: + raise HTTPException(400, "No valid evaluated model is active. Supply a trained checkpoint or initialize the local model registry.") from error + try: + normalized_plan = normalize_mapping_plan(json.loads(mapping_plan) if mapping_plan else None) + except (ValueError, TypeError) as error: + raise HTTPException(400, f"Invalid mapping plan: {error}") from error if metadata_id: _metadata_manifest(metadata_id) JOBS_DIR.mkdir(parents=True, exist_ok=True) - _cleanup_old_jobs() job_id = uuid.uuid4().hex[:12] destination = _job_dir(job_id) destination.mkdir(parents=True) @@ -622,6 +721,9 @@ async def generate( "profile": "official-premium", "difficulties": chosen, "metadataId": metadata_id, + "mappingPlan": normalized_plan, + "engine": engine, + "rlModel": rl_model, "stages": [], "startedAt": time.time(), "elapsed": 0, @@ -632,6 +734,58 @@ async def generate( return JSONResponse({"id": job_id, "status": "queued"}) +@app.get("/api/jobs") +def job_history(limit: int = Query(50, ge=1, le=200)) -> dict[str, Any]: + jobs = [] + if JOBS_DIR.is_dir(): + for directory in JOBS_DIR.iterdir(): + if not directory.is_dir(): + continue + try: + jobs.append(_public_job(_read_status(directory.name))) + except (OSError, ValueError, KeyError, HTTPException): + continue + jobs.sort(key=lambda job: float(job.get("createdAt", job.get("startedAt", 0))), reverse=True) + return {"jobs": jobs[:limit], "total": len(jobs)} + + +@app.post("/api/jobs/{job_id}/retry") +def job_retry(job_id: str, background_tasks: BackgroundTasks) -> dict[str, Any]: + original = _read_status(job_id) + if original.get("status") in ACTIVE_STATES: + raise HTTPException(409, "Cancel or finish this run before retrying") + if original.get("revisionOf"): + raise HTTPException(409, "Retry a section revision from its original map's section controls") + audio = _audio_path(job_id) + new_id = uuid.uuid4().hex[:12] + destination = _job_dir(new_id) + destination.mkdir(parents=True) + shutil.copy2(audio, destination / audio.name) + for filename in ("anchors.json", "approved_palette.json"): + source = _job_dir(job_id) / filename + if source.is_file(): + shutil.copy2(source, destination / filename) + preserved = {key: original[key] for key in ("title", "artist", "mapper", "seed", "profile", "difficulties", "metadataId", "mappingPlan", "engine", "rlModel", "continueUnconfirmed") if key in original} + status = {**preserved, "id": new_id, "retryOf": job_id, "status": "queued", "stages": [], "startedAt": time.time(), "elapsed": 0, "decodePercent": None} + _write_status(new_id, _append_stage(status, "retry", "Retrying with saved audio, confirmed anchors, palette, and mapping settings.")) + background_tasks.add_task(_run_job, new_id) + return {"id": new_id, "status": "queued", "retryOf": job_id} + + +@app.post("/api/jobs/{job_id}/cancel") +def job_cancel(job_id: str) -> dict[str, Any]: + with STATUS_LOCK: + data = _read_status(job_id) + if data.get("status") not in ACTIVE_STATES: + raise HTTPException(409, "This run has already stopped") + (_job_dir(job_id) / "cancel.requested").touch() + state = "cancelled" if data.get("status") == "queued" else "cancelling" + data = _append_stage(data, state, "Cancellation requested; the generation worker is stopping.") + data.update(status=state, localStatus=None) + _write_status(job_id, data) + return {"id": job_id, "status": state} + + @app.get("/api/jobs/{job_id}") def job_status(job_id: str) -> dict[str, Any]: data = _read_status(job_id) @@ -642,10 +796,9 @@ def job_status(job_id: str) -> dict[str, Any]: "release_candidate", }: data = _update_release_status(job_id) - data.pop("trace", None) if data.get("startedAt") and data.get("status") in {"queued", "running", "validating"}: data["elapsed"] = round(time.time() - float(data["startedAt"]), 2) - return data + return _public_job(data) @app.get("/api/jobs/{job_id}/events") @@ -656,7 +809,7 @@ def stream() -> Any: previous = "" deadline = time.time() + 3600 while time.time() < deadline: - data = _read_status(job_id) + data = _public_job(_read_status(job_id)) if data.get("startedAt") and data.get("status") in {"queued", "running", "validating"}: data = {**data, "elapsed": round(time.time() - float(data["startedAt"]), 2)} encoded = json.dumps(data, separators=(",", ":")) @@ -680,15 +833,25 @@ def job_anchors(job_id: str, payload: dict[str, Any], background_tasks: Backgrou if not isinstance(anchors, list) or len(anchors) < 2: raise HTTPException(400, "At least two anchors are required") last_beat = last_sample = None + normalized_anchors = [] for index, anchor in enumerate(anchors): if not isinstance(anchor, dict) or "beat" not in anchor or not ({"sample", "timeSeconds"} & set(anchor)): raise HTTPException(400, f"Anchor {index} requires beat and sample or timeSeconds") - beat = float(anchor["beat"]) - sample = int(anchor.get("sample", round(float(anchor["timeSeconds"]) * 44100))) + try: + beat = float(anchor["beat"]) + raw_sample = float(anchor["sample"]) if "sample" in anchor else float(anchor["timeSeconds"]) * 44100 + if not math.isfinite(beat) or not math.isfinite(raw_sample) or beat < 0 or raw_sample < 0: + raise ValueError("beat and time must be finite nonnegative numbers") + if "sample" in anchor and not raw_sample.is_integer(): + raise ValueError("sample must be an integer") + sample = round(raw_sample) + except (TypeError, ValueError, OverflowError) as error: + raise HTTPException(400, f"Invalid anchor {index}: {error}") from error if last_beat is not None and (beat <= last_beat or sample <= int(last_sample)): raise HTTPException(400, "Anchors must increase strictly in beat and sample") last_beat, last_sample = beat, sample - (_job_dir(job_id) / "anchors.json").write_text(json.dumps({"anchors": anchors}, indent=2), encoding="utf-8") + normalized_anchors.append({**anchor, "beat": beat, "sample": sample}) + _write_json(_job_dir(job_id) / "anchors.json", {"anchors": normalized_anchors}) status["status"] = "queued" status = _append_stage(status, "anchors", "confirmed anchors saved; refitting the beat grid") _write_status(job_id, status) @@ -828,6 +991,8 @@ def job_download(job_id: str) -> FileResponse: data = _read_status(job_id) if data.get("localStatus") not in {"playtest_candidate", "unconfirmed_pack"}: raise HTTPException(409, "Map package is not ready to download") + if _qa_report(_job_dir(job_id) / "map")["errors"]: + raise HTTPException(409, "Map package is blocked by a missing or failed QA report") path = _job_dir(job_id) / "map.zip" if not path.is_file(): raise HTTPException(404, "Map package is missing") @@ -873,11 +1038,10 @@ def import_installed_map(payload: ImportInstalledMap) -> dict[str, Any]: raise HTTPException(409, "Only BeatForge-generated folders with a QA report can be attached") try: info = json.loads(info_path.read_text(encoding="utf-8-sig")) - qa = json.loads(qa_path.read_text(encoding="utf-8")) + qa = _qa_report(source_dir) except (OSError, json.JSONDecodeError) as error: raise HTTPException(409, f"Installed map metadata is unreadable: {error}") from error JOBS_DIR.mkdir(parents=True, exist_ok=True) - _cleanup_old_jobs() job_id = uuid.uuid4().hex[:12] job_dir = _job_dir(job_id) map_dir = job_dir / "map" @@ -1048,13 +1212,16 @@ def playtests(job_id: str, evidence: PlaytestEvidence) -> dict[str, Any]: raise HTTPException(409, "Install and validate a playtest candidate first") if evidence.difficulty not in {"Easy", "Normal", "Hard", "Expert", "ExpertPlus"}: raise HTTPException(400, "Unknown difficulty") + if evidence.difficulty not in data.get("difficulties", STUDIO_DIFFICULTIES): + raise HTTPException(400, "This difficulty is not present in this map") if evidence.speed not in {"slow", "full"}: raise HTTPException(400, "Speed must be slow or full") path = _job_dir(job_id) / "playtests.json" - items = json.loads(path.read_text(encoding="utf-8")) if path.is_file() else [] - items.append(evidence.model_dump(by_alias=True) | {"recordedAt": time.time()}) - path.write_text(json.dumps(items, indent=2), encoding="utf-8") - status = _update_release_status(job_id) + with STATUS_LOCK: + items = json.loads(path.read_text(encoding="utf-8")) if path.is_file() else [] + items.append(evidence.model_dump(by_alias=True) | {"recordedAt": time.time()}) + _write_json(path, items) + status = _update_release_status(job_id) return {"status": status["status"], "releaseGate": status.get("releaseGate"), "evidence": items} @@ -1068,3 +1235,12 @@ def job_click_track(job_id: str) -> FileResponse: if not path.is_file(): raise HTTPException(404, "Click track is not available yet") return FileResponse(path, media_type="audio/wav", filename="click_track.wav") + + +from beatforge.preview import router as preview_router + +app.include_router(preview_router) + +from beatforge.learning import router as learning_router + +app.include_router(learning_router) diff --git a/src/beatforge/cli.py b/src/beatforge/cli.py index c140ef0..d5e9402 100644 --- a/src/beatforge/cli.py +++ b/src/beatforge/cli.py @@ -93,6 +93,9 @@ def main(argv: list[str] | None = None) -> int: p.add_argument("--seed", type=int, default=42) p.add_argument("--anchors", type=Path, default=None) p.add_argument("--palette", type=Path, default=None) + p.add_argument("--mapping-plan", type=Path, help="JSON creative controls shared with Studio") + p.add_argument("--engine", choices=("premium", "rl"), default="premium") + p.add_argument("--rl-model", type=Path, help="Trained checkpoint required by the RL engine") p.add_argument( "--no-critic", action="store_true", @@ -112,6 +115,16 @@ def main(argv: list[str] | None = None) -> int: if args.legacy: return _legacy_main(args) + mapping_plan = None + if args.mapping_plan: + from .mapping_plan import normalize_mapping_plan + try: + mapping_plan = normalize_mapping_plan(json.loads(args.mapping_plan.read_text(encoding="utf-8-sig"))) + except (OSError, ValueError, TypeError) as error: + p.error(f"Invalid mapping plan: {error}") + if args.engine == "rl" and not args.rl_model: + p.error("--engine rl requires --rl-model with a trained checkpoint") + title = args.title or audio.stem out_dir = args.out.expanduser().resolve() map_folder = out_dir / "".join(c if c.isalnum() or c in "-_" else "_" for c in title)[:80] @@ -126,6 +139,9 @@ def main(argv: list[str] | None = None) -> int: seed=args.seed, anchors=args.anchors.expanduser().resolve() if args.anchors else None, palette=args.palette.expanduser().resolve() if args.palette else None, + mapping_plan=mapping_plan, + engine=args.engine, + rl_model=args.rl_model.expanduser().resolve() if args.rl_model else None, progress=lambda stage, detail: print(f" [{stage}] {detail}"), ) print(f"Studio status: {result['status']} (exit {result['returnCode']})") diff --git a/src/beatforge/connector.py b/src/beatforge/connector.py new file mode 100644 index 0000000..3f7ba69 --- /dev/null +++ b/src/beatforge/connector.py @@ -0,0 +1,395 @@ +"""Isolated MCP/REST gateway. Never mounts the privileged local Studio API.""" +from __future__ import annotations + +import json +import os +import secrets +import sqlite3 +from contextlib import asynccontextmanager +from contextvars import ContextVar +from pathlib import Path + +import anyio +import jwt +from fastapi import FastAPI, Request, Header +from fastapi.openapi.utils import get_openapi +from mcp.server.fastmcp import FastMCP +from mcp.server.transport_security import TransportSecuritySettings +from mcp.types import ToolAnnotations +from starlette.responses import JSONResponse, FileResponse +from starlette.middleware.cors import CORSMiddleware +from starlette.middleware.trustedhost import TrustedHostMiddleware + +from beatforge.connector_auth import OAuthSettings, OAuthVerifier, Principal +from beatforge.connector_store import ConnectorStore +from beatforge.connector_uploads import UploadRequest, Uploads +from beatforge.connector_jobs import GenerationRequest, WorkerResult, SectionRevision +from beatforge.connector_artifacts import Artifacts +from beatforge.connector_logging import protect_download_logs +from beatforge.connector_limits import bounded_request +from beatforge.connector_editorial import Editorial, ConnectorFeedback, ConnectorComparison, PresetRequest, SuggestionsRequest +from beatforge.mapping_plan import normalize_mapping_plan + + +principal: ContextVar[Principal | None] = ContextVar('beatforge_principal', default=None) + + +def require(scope: str) -> Principal: + actor = principal.get() + if actor is None or scope not in actor.scopes: + raise PermissionError('This connection does not have the required scope') + return actor + + +class Operations: + def __init__(self, store: ConnectorStore): + self.store = store + self.artifacts = Artifacts(store, store.path.parent / 'artifacts') + self.editorial = Editorial(store, self.artifacts) + + def list_jobs(self, limit: int = 20, offset: int = 0) -> dict: + """List only this account's saved mapping runs, newest first.""" + return self.store.list_jobs(require('read').owner, limit, offset) + + def get_qa(self, job_id: str) -> dict: + """Read structural findings. These are not human headset evidence.""" + return self.artifacts.read_json(require('read').owner, job_id, 'qa.json') + + def get_preview(self, job_id: str, difficulty: str = 'Hard') -> dict: + """Read a chart preview and short-lived audio link for the human editor.""" + owner = require('read').owner + payload = self.editorial.preview(owner, job_id, difficulty) + audio = self.artifacts.named(owner, job_id, 'audio.ogg') + link = self.artifacts.link(owner, job_id, audio['id']) + return {**payload, 'jobId': job_id, 'audioUrl': link['downloadUrl'], 'audioExpiresAt': link['expiresAt']} + + def get_preview_summary(self, job_id: str, difficulty: str = 'Hard') -> dict: + """Inspect preview identity and counts without sending the full chart through MCP.""" + payload = self.get_preview(job_id, difficulty) + return {key: payload[key] for key in ('jobId', 'title', 'artist', 'difficulty', 'difficulties', + 'chartHash', 'audioHash', 'bpm', 'duration', 'timingVerified', 'sections')} | { + 'noteCount': len(payload['chart'].get('colorNotes', [])), + 'previewPath': f'/api/jobs/{job_id}/preview?difficulty={difficulty}', + 'humanPlaytestRequired': True} + + def record_feedback(self, feedback: ConnectorFeedback) -> dict: + """Record only ratings, tags and notes explicitly supplied by a person. Never invent them.""" + return self.editorial.feedback(require('feedback').owner, feedback) + + def record_comparison(self, comparison: ConnectorComparison) -> dict: + """Save a person's explicit preference between two charts of the same audio and difficulty.""" + return self.editorial.compare(require('feedback').owner, comparison) + + def save_preset(self, preset: PresetRequest) -> dict: + """Save or replace this account's named musical style.""" + return self.editorial.preset(require('edit').owner, preset) + + def list_presets(self) -> dict: + return {'presets': self.editorial.records(require('read').owner, 'preset')} + + def feedback_suggestions(self, request: SuggestionsRequest) -> dict: + """Suggest bounded changes from explicit feedback on distinct charts. Does not apply or train.""" + return self.editorial.suggestions(require('read').owner, request) + + def learning_summary(self) -> dict: + owner = require('read').owner + return {'feedbackCount': len(self.editorial.records(owner, 'feedback')), + 'comparisonCount': len(self.editorial.records(owner, 'comparison')), + 'presetCount': len(self.editorial.records(owner, 'preset')), + 'automaticTraining': False, 'humanPlaytestRequired': True} + + def export_learning(self) -> dict: + """Export this account's explicit evidence and presets for local evaluation.""" + owner = require('read').owner + return {'schemaVersion': 1, + 'feedback': self.editorial.records(owner, 'feedback'), + 'comparisons': self.editorial.records(owner, 'comparison'), + 'presets': self.editorial.records(owner, 'preset'), + 'acceptedRevisions': [], 'automaticTraining': False} + + def get_artifacts(self, job_id: str) -> dict: + """Return completed outputs and five-minute download links. Treat links as private.""" + owner = require('read').owner + items = self.artifacts.published(owner, job_id) + return {'artifacts': [{**item, 'downloadPath': f"/api/jobs/{job_id}/artifacts/{item['id']}", + **self.artifacts.link(owner, job_id, item['id'])} for item in items]} + + def capabilities(self) -> dict: + online = self.store.worker_online(require('read').owner) + return {'name': 'BeatForge', 'version': 1, 'editorialControls': True, + 'transport': 'streamable-http', 'generationAvailable': online, + 'generationMessage': 'Worker connected.' if online else 'Your local worker is offline. Jobs wait until it reconnects.', + 'humanPlaytestRequired': True} + + def resolve_plan(self, plan: dict) -> dict: + require('read') + return normalize_mapping_plan(plan) + + def get_job(self, job_id: str) -> dict: + return self.store.get(require('read').owner, job_id) + + def cancel_job(self, job_id: str) -> dict: + return self.store.cancel(require('edit').owner, job_id) + + def create_upload_session(self, upload: UploadRequest) -> dict: + """Reserve audio storage. Upload raw bytes to uploadPath with this account's bearer token.""" + result = self.store.reserve_upload(require('generate').owner, upload.filename, upload.size) + return {**result, 'uploadPath': f"/api/uploads/{result['id']}/content", 'method': 'PUT', + 'contentType': 'application/octet-stream'} + + def get_upload(self, upload_id: str) -> dict: + return self.store.get_upload(require('read').owner, upload_id) + + def start_generation(self, generation: GenerationRequest) -> dict: + """Queue a map from an owned upload. The worker may require timing anchors or corpus setup.""" + owner = require('generate').owner + upload = self.store.get_upload(owner, generation.uploadId) + if upload['state'] != 'ready': + raise ValueError('Finish uploading the audio before generation') + payload = generation.model_dump(exclude={'idempotencyKey'}) + payload['mappingPlan'] = normalize_mapping_plan(generation.mappingPlan) + payload['difficulties'] = list(dict.fromkeys(generation.difficulties)) + job = self.store.submit(owner, generation.idempotencyKey, payload) + return {**job, 'workerOnline': self.store.worker_online(owner)} + + def revise_section(self, job_id: str, revision: SectionRevision) -> dict: + """Create a separate section revision on the original worker; never alter the parent map.""" + owner = require('edit').owner + parent = self.store.get(owner, job_id) + if parent['state'] != 'completed' or parent['result']['status'] != 'playtest_candidate': + raise ValueError('Start from a completed map that passed structural validation') + preview = self.editorial.preview(owner, job_id, revision.difficulty) + if preview['chartHash'] != revision.baseHash: + raise ValueError('The map changed. Reload the preview before revising') + if preview['chart'].get('bpmEvents'): + raise ValueError('Section revisions require a constant exported BPM clock') + if revision.endBeat * 60 / preview['bpm'] > preview['duration'] + 1e-6: + raise ValueError('The selected range extends beyond the song') + data = revision.model_dump(exclude={'idempotencyKey'}) + data['mappingPlan'] = normalize_mapping_plan(revision.mappingPlan) + payload = {**parent['request'], 'revision': {**data, 'parentJob': job_id}, + 'mappingPlan': data['mappingPlan'], 'seed': revision.seed} + return self.store.submit(owner, revision.idempotencyKey, payload) + + +class TokenAuth: + """Authenticate every request before either API adapter is invoked.""" + def __init__(self, app, credentials: dict[str, Principal], verifier: OAuthVerifier | None = None): + self.app, self.credentials, self.verifier = app, credentials, verifier + + async def __call__(self, scope, receive, send): + public_paths = {'/health', '/.well-known/oauth-protected-resource', '/.well-known/oauth-protected-resource/mcp'} + if scope['type'] != 'http' or scope.get('path') in public_paths or scope.get('path', '').startswith('/downloads/'): + return await self.app(scope, receive, send) + headers = dict(scope.get('headers', [])) + authorization = headers.get(b'authorization', b'').decode('latin-1') + scheme, _, supplied = authorization.partition(' ') + if scheme.lower() != 'bearer' or not supplied.isascii() or len(supplied) > 16384: + supplied = '' + actor = next((value for key, value in self.credentials.items() if secrets.compare_digest(key, supplied)), None) + if actor is None and supplied and self.verifier: + try: + actor = await anyio.to_thread.run_sync(self.verifier.verify, supplied) + except jwt.PyJWTError: + actor = None + if actor is None: + challenge = 'Bearer' + if self.verifier: + challenge += f' resource_metadata="{self.verifier.settings.metadata_url}"' + return await JSONResponse({'detail': 'Authentication required'}, status_code=401, + headers={'WWW-Authenticate': challenge, 'Cache-Control': 'no-store'})(scope, receive, send) + token = principal.set(actor) + try: + await bounded_request(self.app, scope, receive, send) + finally: + principal.reset(token) + + +def create_app(*, database: Path, credentials: dict[str, Principal] | None = None, + hosts: list[str] | None = None, origins: list[str] | None = None, + oauth: OAuthSettings | None = None): + protect_download_logs() + credentials = credentials or {} + if (not credentials and not oauth) or any(len(key) < 32 or not key.isascii() or not actor.owner for key, actor in credentials.items()): + raise ValueError('Configure nonempty owners and tokens of at least 32 characters') + hosts = hosts or ['127.0.0.1:*', 'localhost:*'] + origins = origins or [] + if '*' in hosts or '*' in origins: + raise ValueError('Configure explicit connector hosts and browser origins') + verifier = OAuthVerifier(oauth) if oauth else None + operations = Operations(ConnectorStore(database)) + uploads = Uploads(operations.store, database.parent / 'uploads') + mcp = FastMCP('BeatForge', instructions='Preserve the user creative brief. Never invent ratings or headset evidence.', + stateless_http=True, json_response=True, + transport_security=TransportSecuritySettings(allowed_hosts=hosts, allowed_origins=origins)) + read = ToolAnnotations(readOnlyHint=True, destructiveHint=False, openWorldHint=False) + mcp.tool(name='get_capabilities', annotations=read)(operations.capabilities) + mcp.tool(name='resolve_mapping_plan', annotations=read)(operations.resolve_plan) + mcp.tool(name='get_job', annotations=read)(operations.get_job) + mcp.tool(name='get_artifacts', annotations=read)(operations.get_artifacts) + for name, handler in [('list_jobs', operations.list_jobs), ('get_qa', operations.get_qa), + ('get_chart_preview', operations.get_preview_summary), ('list_presets', operations.list_presets), + ('get_learning_summary', operations.learning_summary)]: + mcp.tool(name=name, annotations=read)(handler) + write = ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=True, openWorldHint=False) + mcp.tool(name='record_mapping_feedback', annotations=write)(operations.record_feedback) + mcp.tool(name='record_comparison', annotations=write)(operations.record_comparison) + mcp.tool(name='save_preset', annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=True, idempotentHint=True, openWorldHint=False))(operations.save_preset) + mcp.tool(name='suggest_mapping_adjustments', annotations=read)(operations.feedback_suggestions) + mcp.tool(name='get_upload', annotations=read)(operations.get_upload) + mcp.tool(name='create_upload_session', annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False, openWorldHint=False))(operations.create_upload_session) + mcp.tool(name='start_generation', annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=True, openWorldHint=False))(operations.start_generation) + mcp.tool(name='revise_section', annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=True))(operations.revise_section) + mcp.tool(name='cancel_job', annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=True, idempotentHint=True))(operations.cancel_job) + transport = mcp.streamable_http_app() + + @asynccontextmanager + async def lifespan(app): + async with mcp.session_manager.run(): + yield + + app = FastAPI(title='BeatForge connector', lifespan=lifespan) + + def connector_schema(): + if app.openapi_schema is None: + schema = get_openapi(title=app.title, version='0.1.0', routes=app.routes, + description='Account-scoped BeatForge API. Use a bearer credential from the configured OAuth provider or a private pilot token. Human feedback must be explicitly supplied; generated maps still require playtesting.') + schema.setdefault('components', {}).setdefault('securitySchemes', {})['BearerAuth'] = { + 'type': 'http', 'scheme': 'bearer', 'description': 'Scoped BeatForge access token. Never put it in a URL.'} + schema['security'] = [{'BearerAuth': []}] + for path, operations_schema in schema['paths'].items(): + if path == '/health' or path.startswith('/.well-known/') or path.startswith('/downloads/'): + for operation in operations_schema.values(): + if isinstance(operation, dict): + operation['security'] = [] + app.openapi_schema = schema + return app.openapi_schema + + app.openapi = connector_schema + + @app.exception_handler(sqlite3.OperationalError) + async def storage_unavailable(request, error): + return JSONResponse({'detail': 'Gateway metadata storage is unavailable. Contact the operator.'}, status_code=503) + + @app.exception_handler(PermissionError) + async def permission_error(request, error): + return JSONResponse({'detail': str(error)}, status_code=403) + + @app.exception_handler(KeyError) + async def missing(request, error): + return JSONResponse({'detail': 'Resource not found'}, status_code=404) + + @app.exception_handler(ValueError) + async def invalid(request, error): + return JSONResponse({'detail': str(error)}, status_code=422) + + @app.get('/health') + def health(): + return {'status': 'ok'} + + if oauth: + app.get('/.well-known/oauth-protected-resource')(oauth.metadata) + app.get('/.well-known/oauth-protected-resource/mcp')(oauth.metadata) + + app.get('/api/capabilities')(operations.capabilities) + app.post('/api/uploads')(operations.create_upload_session) + app.get('/api/uploads/{upload_id}')(operations.get_upload) + + @app.put('/api/uploads/{upload_id}/content') + async def receive_upload(upload_id: str, request: Request): + return await uploads.receive(require('generate').owner, upload_id, request) + app.post('/api/mapping-plan/resolve')(operations.resolve_plan) + app.get('/api/jobs/{job_id}')(operations.get_job) + app.post('/api/jobs/{job_id}/cancel')(operations.cancel_job) + app.post('/api/jobs')(operations.start_generation) + app.post('/api/jobs/{job_id}/revise')(operations.revise_section) + app.get('/api/jobs')(operations.list_jobs) + app.get('/api/jobs/{job_id}/preview')(operations.get_preview) + app.get('/api/jobs/{job_id}/qa')(operations.get_qa) + app.get('/api/learning')(operations.learning_summary) + app.get('/api/learning/export')(operations.export_learning) + app.post('/api/learning/suggestions')(operations.feedback_suggestions) + app.post('/api/feedback')(operations.record_feedback) + app.post('/api/comparisons')(operations.record_comparison) + app.post('/api/presets')(operations.save_preset) + app.get('/api/presets')(operations.list_presets) + app.get('/api/jobs/{job_id}/artifacts')(operations.get_artifacts) + + @app.get('/downloads/{artifact_id}') + def signed_download(artifact_id: str, ticket: str): + path, name, media_type = operations.artifacts.signed_download(artifact_id, ticket) + return FileResponse(path, filename=name, media_type=media_type, + headers={'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', + 'Referrer-Policy': 'no-referrer'}) + + @app.get('/api/jobs/{job_id}/artifacts/{artifact_id}') + def download_artifact(job_id: str, artifact_id: str): + path, name, media_type = operations.artifacts.download(require('read').owner, job_id, artifact_id) + return FileResponse(path, filename=name, media_type=media_type, + headers={'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff'}) + + @app.put('/api/worker/jobs/{job_id}/artifacts/{name}') + async def upload_artifact(job_id: str, name: str, request: Request, x_beatforge_lease: str = Header()): + return await operations.artifacts.receive(require('worker').owner, job_id, x_beatforge_lease, name, request) + + @app.post('/api/worker/claim') + def claim_work(): + owner = require('worker').owner + operations.store.worker_seen(owner) + job = operations.store.claim(owner) + if job: + job['upload'] = operations.store.get_upload(owner, job['request']['uploadId']) + return {'job': job} + + @app.post('/api/worker/jobs/{job_id}/heartbeat') + def heartbeat(job_id: str, x_beatforge_lease: str = Header()): + owner = require('worker').owner + operations.store.heartbeat(owner, job_id, x_beatforge_lease) + operations.store.worker_seen(owner) + return {'status': 'ok'} + + @app.get('/api/worker/jobs/{job_id}/audio') + def worker_audio(job_id: str, x_beatforge_lease: str = Header()): + owner = require('worker').owner + job = operations.store.check_lease(owner, job_id, x_beatforge_lease) + path = uploads.path(job['request']['uploadId']) + if not path.is_file(): + raise KeyError('Upload not found') + return FileResponse(path, media_type='application/octet-stream', headers={'Cache-Control': 'no-store'}) + + @app.post('/api/worker/jobs/{job_id}/finish') + def finish_work(job_id: str, result: WorkerResult, x_beatforge_lease: str = Header()): + owner = require('worker').owner + job = operations.store.check_lease(owner, job_id, x_beatforge_lease) + manifest = operations.artifacts.manifest(owner, job_id, x_beatforge_lease) + if result.status == 'playtest_candidate': + required = {'map.zip', 'audio.ogg', 'qa.json'} | {f'preview-{name}.json' for name in job['request']['difficulties']} + if not required.issubset({item['name'] for item in manifest}): + raise ValueError('Upload map, audio, QA and every difficulty preview before completing this job') + return operations.store.finish(owner, job_id, x_beatforge_lease, + {**result.model_dump(), 'artifacts': manifest}, failed=result.status == 'error') + app.mount('/', transport) + app.add_middleware(TokenAuth, credentials=credentials, verifier=verifier) + app.add_middleware(CORSMiddleware, allow_origins=origins, + allow_methods=['GET', 'POST', 'PUT', 'DELETE'], + allow_headers=['Authorization', 'Content-Type', 'MCP-Protocol-Version', 'Mcp-Session-Id'], + expose_headers=['WWW-Authenticate', 'Mcp-Session-Id']) + app.add_middleware(TrustedHostMiddleware, allowed_hosts=[host.removesuffix(':*') for host in hosts]) + return app + + +def from_env(): + """uvicorn beatforge.connector:from_env --factory --host 127.0.0.1 --port 8013""" + config = json.loads(os.environ.get('BEATFORGE_CONNECTOR_TOKENS', '{}')) + credentials = {token: Principal(value['owner'], frozenset(value['scopes'])) for token, value in config.items()} + oauth = None + if os.environ.get('BEATFORGE_OAUTH_ISSUER'): + oauth = OAuthSettings(issuer=os.environ['BEATFORGE_OAUTH_ISSUER'], + jwks_url=os.environ['BEATFORGE_OAUTH_JWKS_URL'], + resource=os.environ['BEATFORGE_OAUTH_RESOURCE'], + revoked_subjects=frozenset(json.loads(os.environ.get('BEATFORGE_REVOKED_SUBJECTS', '[]')))) + return create_app(database=Path(os.environ.get('BEATFORGE_CONNECTOR_DB', 'data/connector/jobs.sqlite3')), + credentials=credentials, + oauth=oauth, + origins=list(filter(None, os.environ.get('BEATFORGE_CONNECTOR_ORIGINS', '').split(','))), + hosts=os.environ.get('BEATFORGE_CONNECTOR_HOSTS', 'localhost:*,127.0.0.1:*').split(',')) diff --git a/src/beatforge/connector_artifacts.py b/src/beatforge/connector_artifacts.py new file mode 100644 index 0000000..7f9dc70 --- /dev/null +++ b/src/beatforge/connector_artifacts.py @@ -0,0 +1,142 @@ +"""Lease-bound, immutable worker outputs with account storage limits.""" +from __future__ import annotations + +import hashlib +import json +import time +import uuid +import secrets +import jwt +import anyio +from pathlib import Path + +from fastapi import HTTPException, Request + +from beatforge.connector_store import ConnectorStore + +ARTIFACT_TIMEOUT_SECONDS = 300 + +ARTIFACT_TYPES = {'map.zip': 'application/zip', 'audio.ogg': 'audio/ogg', 'qa.json': 'application/json', + **{f'preview-{name}.json': 'application/json' for name in ('Easy', 'Normal', 'Hard', 'Expert', 'ExpertPlus')}} + + +class Artifacts: + def __init__(self, store: ConnectorStore, root: Path): + self.store, self.root = store, root.resolve() + # Short-lived links intentionally expire on restart as well as by time. + self.link_key = secrets.token_bytes(32) + self.root.mkdir(parents=True, exist_ok=True) + with store.connect() as db: + db.execute('''CREATE TABLE IF NOT EXISTS artifacts ( + id TEXT PRIMARY KEY, owner TEXT NOT NULL, job TEXT NOT NULL, + lease TEXT NOT NULL, name TEXT NOT NULL, size INTEGER NOT NULL, + sha256 TEXT, ready INTEGER NOT NULL DEFAULT 0, + UNIQUE(job,lease,name))''') + + def path(self, artifact_id: str) -> Path: + if len(artifact_id) != 32 or any(c not in '0123456789abcdef' for c in artifact_id): + raise KeyError('Artifact not found') + path = (self.root / artifact_id).resolve() + if path.parent != self.root: + raise KeyError('Artifact not found') + return path + + async def receive(self, owner: str, job_id: str, lease: str, name: str, request: Request) -> dict: + if name not in ARTIFACT_TYPES: + raise ValueError('Unsupported artifact') + maximum = 16*1024*1024 if name.endswith('.json') else 64*1024*1024 + length = request.headers.get('content-length', '') + if not length.isdecimal() or not 1 <= int(length) <= maximum: + raise HTTPException(413, 'A bounded Content-Length is required') + size = int(length) + self.store.check_lease(owner, job_id, lease) + artifact_id = uuid.uuid4().hex + with self.store.connect() as db: + db.execute('BEGIN IMMEDIATE') + if db.execute('SELECT id FROM artifacts WHERE job=? AND lease=? AND name=?', (job_id, lease, name)).fetchone(): + raise ValueError('Artifact already uploaded for this lease') + usage = db.execute('SELECT COALESCE(SUM(size),0) FROM artifacts WHERE owner=?', (owner,)).fetchone()[0] + if usage + size > 256*1024*1024: + raise ValueError('Account artifact quota reached') + self.store.check_blob_budget(db, size) + db.execute('INSERT INTO artifacts(id,owner,job,lease,name,size) VALUES(?,?,?,?,?,?)', (artifact_id, owner, job_id, lease, name, size)) + path = self.path(artifact_id) + try: + digest, received = hashlib.sha256(), 0 + try: + with anyio.fail_after(ARTIFACT_TIMEOUT_SECONDS): + with path.open('xb') as stream: + async for chunk in request.stream(): + received += len(chunk) + if received > size: + raise HTTPException(413, 'Artifact exceeds its reserved size') + stream.write(chunk) + digest.update(chunk) + except TimeoutError as error: + raise HTTPException(408, 'Artifact upload timed out; retry with a valid lease') from error + if received != size: + raise HTTPException(422, 'Artifact is incomplete') + with self.store.connect() as db: + db.execute('BEGIN IMMEDIATE') + valid = db.execute("SELECT id FROM jobs WHERE id=? AND owner=? AND lease=? AND lease_until>? AND state='running'", (job_id, owner, lease, time.time())).fetchone() + if not valid: + raise ValueError('Lease expired, revoked, or replaced') + db.execute('UPDATE artifacts SET ready=1,sha256=? WHERE id=?', (digest.hexdigest(), artifact_id)) + except BaseException: + path.unlink(missing_ok=True) + with self.store.connect() as db: + db.execute('DELETE FROM artifacts WHERE id=?', (artifact_id,)) + raise + return {'name': name, 'size': size, 'sha256': digest.hexdigest()} + + def manifest(self, owner: str, job_id: str, lease: str) -> list[dict]: + with self.store.connect() as db: + rows = db.execute('SELECT id,name,size,sha256 FROM artifacts WHERE owner=? AND job=? AND lease=? AND ready=1 ORDER BY name', (owner, job_id, lease)).fetchall() + return [dict(row) for row in rows] + + def published(self, owner: str, job_id: str) -> list[dict]: + job = self.store.get(owner, job_id) + return (job.get('result') or {}).get('artifacts', []) if job['state'] == 'completed' else [] + + def named(self, owner: str, job_id: str, name: str) -> dict: + entry = next((item for item in self.published(owner, job_id) if item['name'] == name), None) + if not entry: + raise KeyError('Artifact not found') + return entry + + def read_json(self, owner: str, job_id: str, name: str) -> dict: + entry = self.named(owner, job_id, name) + if not name.endswith('.json') or entry['size'] > 16*1024*1024: + raise ValueError('Not a readable JSON artifact') + content = self.path(entry['id']).read_bytes() + if len(content) != entry['size'] or hashlib.sha256(content).hexdigest() != entry['sha256']: + raise ValueError('Artifact integrity check failed') + result = json.loads(content) + if not isinstance(result, dict): + raise ValueError('Artifact must contain an object') + return result + + def download(self, owner: str, job_id: str, artifact_id: str): + entry = next((item for item in self.published(owner, job_id) if item['id'] == artifact_id), None) + if not entry or not self.path(artifact_id).is_file(): + raise KeyError('Artifact not found') + return self.path(artifact_id), entry['name'], ARTIFACT_TYPES[entry['name']] + + def link(self, owner: str, job_id: str, artifact_id: str) -> dict: + self.download(owner, job_id, artifact_id) + expires = int(time.time()) + 300 + token = jwt.encode({'sub': owner, 'job': job_id, 'artifact': artifact_id, 'exp': expires, + 'aud': 'beatforge-artifact'}, self.link_key, algorithm='HS256') + return {'downloadUrl': f'/downloads/{artifact_id}?ticket={token}', 'expiresAt': expires} + + def signed_download(self, artifact_id: str, ticket: str): + try: + if len(ticket) > 4096: + raise jwt.InvalidTokenError('Oversized ticket') + claims = jwt.decode(ticket, self.link_key, algorithms=['HS256'], audience='beatforge-artifact', + options={'require': ['sub', 'job', 'artifact', 'exp']}) + if claims['artifact'] != artifact_id: + raise jwt.InvalidTokenError('Artifact mismatch') + return self.download(claims['sub'], claims['job'], artifact_id) + except jwt.PyJWTError as error: + raise HTTPException(401, 'Download link expired or invalid') from error diff --git a/src/beatforge/connector_auth.py b/src/beatforge/connector_auth.py new file mode 100644 index 0000000..67ec672 --- /dev/null +++ b/src/beatforge/connector_auth.py @@ -0,0 +1,79 @@ +"""OAuth resource-server validation; the configured provider owns sign-in and consent.""" +from __future__ import annotations + +import hashlib +import json +from dataclasses import dataclass, field +from urllib.parse import urlsplit + +import jwt +from jwt import PyJWKClient + +USER_SCOPES = frozenset({'read', 'generate', 'edit', 'feedback'}) + + +@dataclass(frozen=True) +class Principal: + owner: str + scopes: frozenset[str] + + +def https_url(value: str) -> str: + parsed = urlsplit(value) + if (parsed.scheme != 'https' or not parsed.hostname or parsed.username or + parsed.password or parsed.query or parsed.fragment or + any(ord(char) < 33 or ord(char) > 126 or char in '"\\' for char in value)): + raise ValueError('OAuth URLs must be absolute HTTPS URLs without credentials, query or fragment') + return value + + +@dataclass(frozen=True) +class OAuthSettings: + issuer: str + jwks_url: str + resource: str + revoked_subjects: frozenset[str] = field(default_factory=frozenset) + + def __post_init__(self): + for url in (self.issuer, self.jwks_url, self.resource): + https_url(url) + if urlsplit(self.resource).path != '/mcp': + raise ValueError('The connector resource must identify its /mcp endpoint') + + @property + def metadata_url(self) -> str: + parsed = urlsplit(self.resource) + return f'{parsed.scheme}://{parsed.netloc}/.well-known/oauth-protected-resource/mcp' + + def metadata(self) -> dict: + return {'resource': self.resource, 'resource_name': 'BeatForge', + 'authorization_servers': [self.issuer], + 'scopes_supported': sorted(USER_SCOPES), + 'bearer_methods_supported': ['header']} + + +class OAuthVerifier: + def __init__(self, settings: OAuthSettings): + self.settings = settings + # Key discovery is fixed by deployment configuration, never by token jku/x5u. + self.jwks = PyJWKClient(settings.jwks_url, cache_jwk_set=True, lifespan=300, timeout=5) + + def verify(self, token: str) -> Principal: + header = jwt.get_unverified_header(token) + if header.get('alg') != 'RS256': + raise jwt.InvalidAlgorithmError('Unsupported access-token signing algorithm') + key = self.jwks.get_signing_key_from_jwt(token).key + claims = jwt.decode(token, key, algorithms=['RS256'], issuer=self.settings.issuer, + audience=self.settings.resource, + options={'require': ['exp', 'iat', 'sub', 'iss', 'aud']}) + subject = claims['sub'] + if not isinstance(subject, str) or not subject or subject in self.settings.revoked_subjects: + raise jwt.InvalidTokenError('Account access revoked or invalid subject') + scope = claims.get('scope', '') + if not isinstance(scope, str): + raise jwt.InvalidTokenError('Invalid scope claim') + # Worker authority is provisioned separately and cannot be requested by an assistant. + scopes = frozenset(scope.split()) & USER_SCOPES + identity = json.dumps([self.settings.issuer, subject], separators=(',', ':')) + owner = 'oauth:' + hashlib.sha256(identity.encode()).hexdigest() + return Principal(owner, scopes) diff --git a/src/beatforge/connector_editorial.py b/src/beatforge/connector_editorial.py new file mode 100644 index 0000000..e379c52 --- /dev/null +++ b/src/beatforge/connector_editorial.py @@ -0,0 +1,92 @@ +"""Account-owned editorial evidence bound to immutable published charts.""" +import json +import time +import uuid +from datetime import datetime, timezone + +from pydantic import Field + +from beatforge.connector_artifacts import Artifacts +from beatforge.connector_store import ConnectorStore +from beatforge.editorial_models import FeedbackRequest, ComparisonRequest, PresetRequest, SuggestionsRequest +from beatforge.feedback_guidance import derive_feedback_adjustment +from beatforge.mapping_plan import normalize_mapping_plan + + +class ConnectorFeedback(FeedbackRequest): + idempotencyKey: str = Field(min_length=1, max_length=120) + + +class ConnectorComparison(ComparisonRequest): + idempotencyKey: str = Field(min_length=1, max_length=120) + + +class Editorial: + def __init__(self, store: ConnectorStore, artifacts: Artifacts): + self.store, self.artifacts = store, artifacts + with store.connect() as db: + db.execute('''CREATE TABLE IF NOT EXISTS editorial ( + id TEXT PRIMARY KEY, owner TEXT NOT NULL, kind TEXT NOT NULL, + request_key TEXT NOT NULL, payload TEXT NOT NULL, created REAL NOT NULL, + UNIQUE(owner,kind,request_key))''') + + def save(self, owner, kind, key, payload, *, replace=False): + encoded = json.dumps(payload, sort_keys=True, separators=(',', ':'), allow_nan=False) + with self.store.connect() as db: + db.execute('BEGIN IMMEDIATE') + old = db.execute('SELECT * FROM editorial WHERE owner=? AND kind=? AND request_key=?', (owner, kind, key)).fetchone() + if old: + if not replace and old['payload'] != encoded: + raise ValueError('Idempotency key belongs to different feedback') + if replace: + db.execute('UPDATE editorial SET payload=? WHERE id=?', (encoded, old['id'])) + return {'id': old['id'], **json.loads(encoded if replace else old['payload']), 'createdAt': old['created']} + count = db.execute('SELECT COUNT(*) FROM editorial WHERE owner=?', (owner,)).fetchone()[0] + if count >= 1000: + raise ValueError('Account editorial record limit reached') + record_id, now = uuid.uuid4().hex, time.time() + db.execute('INSERT INTO editorial VALUES(?,?,?,?,?,?)', (record_id, owner, kind, key, encoded, now)) + return {'id': record_id, **payload, 'createdAt': now} + + def records(self, owner, kind): + with self.store.connect() as db: + rows = db.execute('SELECT * FROM editorial WHERE owner=? AND kind=? ORDER BY created DESC,id DESC', (owner, kind)).fetchall() + return [{'id': row['id'], **json.loads(row['payload']), 'createdAt': row['created']} for row in rows] + + def preview(self, owner, job_id, difficulty): + if difficulty not in {'Easy', 'Normal', 'Hard', 'Expert', 'ExpertPlus'}: + raise ValueError('Unknown difficulty') + payload = self.artifacts.read_json(owner, job_id, f'preview-{difficulty}.json') + if payload.get('difficulty') != difficulty or not payload.get('chartHash') or not payload.get('audioHash'): + raise ValueError('Preview identity is invalid') + return payload + + def feedback(self, owner: str, request: ConnectorFeedback): + preview = self.preview(owner, request.jobId, request.difficulty) + payload = request.model_dump(exclude={'idempotencyKey'}, exclude_none=True) + payload.update(chartHash=preview['chartHash'], audioHash=preview['audioHash'], + previewSha256=self.artifacts.named(owner, request.jobId, f'preview-{request.difficulty}.json')['sha256'], + source='human', schemaVersion=1) + return self.save(owner, 'feedback', request.idempotencyKey, payload) + + def compare(self, owner: str, request: ConnectorComparison): + if request.preferredJob == request.alternateJob: + raise ValueError('Choose two different maps') + a = self.preview(owner, request.preferredJob, request.difficulty) + b = self.preview(owner, request.alternateJob, request.difficulty) + if any(a.get(key) != b.get(key) for key in ('audioHash', 'bpm', 'offsetSeconds')): + raise ValueError('Compare the same audio, timing and difficulty') + if a['chart'] == b['chart']: + raise ValueError('The selected charts are identical') + payload = request.model_dump(exclude={'idempotencyKey'}) + payload.update(preferredHash=a['chartHash'], alternateHash=b['chartHash'], audioHash=a['audioHash']) + return self.save(owner, 'comparison', request.idempotencyKey, payload) + + def preset(self, owner: str, request: PresetRequest): + payload = {'name': request.name, 'mappingPlan': normalize_mapping_plan(request.mappingPlan)} + return self.save(owner, 'preset', request.name.casefold(), payload, replace=True) + + def suggestions(self, owner: str, request: SuggestionsRequest): + records = [{**row, 'createdAt': datetime.fromtimestamp(row['createdAt'], timezone.utc).isoformat()} + for row in self.records(owner, 'feedback')] + return derive_feedback_adjustment(records, request.mappingPlan, tester=request.tester, difficulty=request.difficulty) diff --git a/src/beatforge/connector_export.py b/src/beatforge/connector_export.py new file mode 100644 index 0000000..20a6d30 --- /dev/null +++ b/src/beatforge/connector_export.py @@ -0,0 +1,59 @@ +"""Publish only gameplay files and selected review data, never local pipeline logs.""" +from __future__ import annotations + +import json +import shutil +import zipfile +from pathlib import Path + +from beatforge.preview import chart_refs, confined, preview_payload, read_json + + +def prepare_artifacts(folder: Path, destination: Path, mapping_plan: dict) -> dict[str, Path]: + destination.mkdir(parents=True, exist_ok=True) + info = read_json(confined(folder, 'Info.dat')) + refs = chart_refs(info) + names = {'Info.dat', info['_songFilename']} + if info.get('_coverImageFilename'): + names.add(info['_coverImageFilename']) + for ref in refs: + names.add(ref['_beatmapFilename']) + if ref.get('_lightshowDataFilename'): + names.add(ref['_lightshowDataFilename']) + # A portable pack uses flat gameplay filenames, not copied directory trees. + if any(not isinstance(name, str) or '/' in name or '\\' in name or ':' in name or name in {'.', '..'} for name in names): + raise ValueError('Generated pack contains a nonportable filename') + outputs = {'map.zip': destination/'map.zip', 'audio.ogg': destination/'audio.ogg'} + with zipfile.ZipFile(outputs['map.zip'], 'w', compression=zipfile.ZIP_DEFLATED) as archive: + for name in sorted(names): + archive.write(confined(folder, name), name) + shutil.copyfile(confined(folder, info['_songFilename']), outputs['audio.ogg']) + qa = read_json(confined(folder, '_beatforge/qa_report.json')) + + def public_finding(item): + public = {key: item[key] for key in ('code', 'beat', 'difficulty', 'severity') if key in item} + # Detailed local diagnostic messages may contain host paths. + public['message'] = str(item.get('code', 'Review finding')).replace('_', ' ') + return public + + qa_public = {'status': qa.get('status'), 'errors': [public_finding(item) for item in qa.get('errors', [])], + 'warnings': [public_finding(item) for item in qa.get('warnings', [])], 'humanPlaytestRequired': True} + outputs['qa.json'] = destination/'qa.json' + outputs['qa.json'].write_text(json.dumps(qa_public, allow_nan=False), encoding='utf-8') + for ref in refs: + name = ref['_difficulty'] + if name not in {'Easy', 'Normal', 'Hard', 'Expert', 'ExpertPlus'}: + raise ValueError('Unsupported exported difficulty') + payload = preview_payload(folder, name) + payload.pop('provenance', None) + payload['mappingPlan'] = mapping_plan + payload['sections'] = [{key: item[key] for key in ('startBeat', 'endBeat', 'type', 'label') if key in item} for item in payload['sections']] + payload['findings'] = [public_finding(item) for item in payload['findings']] + analysis_path = folder / '_beatforge/analysis.json' + payload['canRevise'] = (analysis_path.is_file() and + len(read_json(analysis_path).get('beatGrid', [])) >= 2 and + (folder / '_beatforge/sections.json').is_file() and + not payload['chart'].get('bpmEvents')) + outputs[f'preview-{name}.json'] = destination/f'preview-{name}.json' + outputs[f'preview-{name}.json'].write_text(json.dumps(payload, allow_nan=False), encoding='utf-8') + return outputs diff --git a/src/beatforge/connector_jobs.py b/src/beatforge/connector_jobs.py new file mode 100644 index 0000000..d2c497b --- /dev/null +++ b/src/beatforge/connector_jobs.py @@ -0,0 +1,44 @@ +"""Constrained wire contracts for generation and outbound workers.""" +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +Difficulty = Literal['Easy', 'Normal', 'Hard', 'Expert', 'ExpertPlus'] + + +class SectionRevision(BaseModel): + model_config = ConfigDict(extra='forbid', allow_inf_nan=False) + idempotencyKey: str = Field(min_length=1, max_length=120) + difficulty: Difficulty + baseHash: str = Field(pattern=r'^[a-f0-9]{64}$') + startBeat: float = Field(ge=0) + endBeat: float = Field(gt=0) + seed: int = Field(default=42, ge=0, le=2147483647) + mappingPlan: dict = Field(default_factory=dict) + + @model_validator(mode='after') + def increasing_range(self): + if self.endBeat <= self.startBeat: + raise ValueError('The end beat must follow the start beat') + return self + + +class GenerationRequest(BaseModel): + model_config = ConfigDict(extra='forbid') + uploadId: str = Field(pattern=r'^[a-f0-9]{32}$') + idempotencyKey: str = Field(min_length=1, max_length=120) + title: str = Field(min_length=1, max_length=200) + artist: str = Field(default='Unknown Artist', max_length=200) + mapper: str = Field(default='BeatForge', max_length=120) + seed: int = Field(default=42, ge=-2147483648, le=2147483647) + difficulties: list[Difficulty] = Field(default_factory=lambda: ['Easy', 'Normal', 'Hard', 'Expert', 'ExpertPlus'], min_length=1, max_length=5) + mappingPlan: dict = Field(default_factory=dict) + allowUnconfirmed: bool = False + + +class WorkerResult(BaseModel): + model_config = ConfigDict(extra='forbid') + status: Literal['playtest_candidate', 'invalid', 'needs_anchors', 'corpus_incomplete', 'needs_palette', 'error'] + message: str = Field(default='', max_length=1000) + # Paths, pipeline logs, corpus data and tokens must never leave the local worker. + humanPlaytestRequired: Literal[True] = True diff --git a/src/beatforge/connector_limits.py b/src/beatforge/connector_limits.py new file mode 100644 index 0000000..dd05a4f --- /dev/null +++ b/src/beatforge/connector_limits.py @@ -0,0 +1,39 @@ +"""Bound authenticated JSON bodies before REST or MCP parsing.""" +import anyio +from starlette.responses import JSONResponse + +MAX_JSON_BYTES = 1024 * 1024 + + +async def bounded_request(app, scope, receive, send): + # PUT handlers already stream against an atomic, size-bounded blob reservation. + if scope.get('method') == 'PUT': + return await app(scope, receive, send) + length = dict(scope.get('headers', [])).get(b'content-length') + if length is not None and (not length.isdigit() or int(length) > MAX_JSON_BYTES): + return await JSONResponse({'detail':'JSON request exceeds 1 MiB'}, status_code=413)(scope, receive, send) + chunks, size = [], 0 + try: + with anyio.fail_after(30): + while True: + message = await receive() + if message['type'] == 'http.disconnect': + return + chunk = message.get('body', b'') + size += len(chunk) + if size > MAX_JSON_BYTES: + return await JSONResponse({'detail':'JSON request exceeds 1 MiB'}, status_code=413)(scope, receive, send) + chunks.append(chunk) + if not message.get('more_body', False): + break + except TimeoutError: + return await JSONResponse({'detail':'Request body timed out'}, status_code=408)(scope, receive, send) + body = b''.join(chunks) + delivered = False + async def replay(): + nonlocal delivered + if not delivered: + delivered = True + return {'type':'http.request', 'body':body, 'more_body':False} + return await receive() + await app(scope, replay, send) diff --git a/src/beatforge/connector_logging.py b/src/beatforge/connector_logging.py new file mode 100644 index 0000000..2f3f497 --- /dev/null +++ b/src/beatforge/connector_logging.py @@ -0,0 +1,24 @@ +"""Keep private download tickets out of application HTTP access logs.""" +import logging + + +class DownloadTicketFilter(logging.Filter): + def filter(self, record): + def redact(value): + text = str(value) + if '/downloads/' in text and '?ticket=' in text: + return text.split('?ticket=', 1)[0] + '?ticket=[redacted]' + return value + if isinstance(record.args, tuple): + record.args = tuple(redact(value) for value in record.args) + elif isinstance(record.args, dict): + record.args = {key: redact(value) for key, value in record.args.items()} + record.msg = redact(record.msg) + return True + + +def protect_download_logs(): + for name in ('uvicorn.access', 'httpx'): + logger = logging.getLogger(name) + if not any(isinstance(item, DownloadTicketFilter) for item in logger.filters): + logger.addFilter(DownloadTicketFilter()) diff --git a/src/beatforge/connector_maintenance.py b/src/beatforge/connector_maintenance.py new file mode 100644 index 0000000..8283215 --- /dev/null +++ b/src/beatforge/connector_maintenance.py @@ -0,0 +1,106 @@ +"""Offline recovery of interrupted uploads; never removes completed source audio.""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from beatforge.connector_store import ConnectorStore + + +def recover_uploads(database: Path, *, apply: bool = False) -> dict: + """The operator must stop the gateway before applying this operation. + + SQLite locks cannot stop a process already streaming to a filesystem handle. + Recovery therefore deliberately has no REST/MCP or automatic startup adapter. + """ + database = Path(database).resolve(strict=True) + store = ConnectorStore(database) + root = database.parent / 'uploads' + if root.is_symlink() or (root.exists() and root.resolve() != root): + raise ValueError('Upload directory must not redirect outside the database directory') + with store.connect() as db: + db.execute('BEGIN EXCLUSIVE') + rows = db.execute("SELECT id FROM uploads WHERE state='writing' ORDER BY id").fetchall() + paths = [] + for row in rows: + identifier = row['id'] + if len(identifier) != 32 or any(c not in '0123456789abcdef' for c in identifier): + raise ValueError('Invalid upload identifier; recovery stopped') + for suffix in ('.part', '.audio'): + candidate = root / (identifier + suffix) + if candidate.is_symlink() or candidate.resolve().parent != root: + raise ValueError('Upload path redirects outside its storage directory') + if candidate.exists() and not candidate.is_file(): + raise ValueError('Upload path is not a regular file') + paths.append(candidate) + report = {'mode':'apply' if apply else 'dry-run', 'interruptedUploads':len(rows), + 'partialFiles':sum(p.exists() for p in paths), 'completedUploadsTouched':0} + if apply: + # Validate the complete set before changing any files. If interrupted + # here, metadata stays writing and rerunning recovery is safe. + for candidate in paths: + candidate.unlink(missing_ok=True) + db.execute("UPDATE uploads SET state='pending',sha256=NULL WHERE state='writing'") + return report + + +def reclaim_artifacts(database: Path, *, apply: bool = False) -> dict: + """Offline only: reclaim attempts no longer referenced by any result or lease.""" + database = Path(database).resolve(strict=True) + store = ConnectorStore(database) + root = database.parent / 'artifacts' + if root.is_symlink() or (root.exists() and root.resolve() != root): + raise ValueError('Artifact directory must not redirect') + with store.connect() as db: + db.execute('BEGIN EXCLUSIVE') + protected, active = set(), set() + for job in db.execute('SELECT id,state,lease,result FROM jobs'): + if job['state'] == 'running': + active.add((job['id'], job['lease'])) + if job['result']: + result = json.loads(job['result']) + # Invalid metadata fails closed; never guess which bytes are safe. + for entry in result.get('artifacts', []): + protected.add(entry['id']) + rows = [] + if db.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='artifacts'").fetchone(): + rows = [r for r in db.execute('SELECT id,job,lease,size FROM artifacts') + if r['id'] not in protected and (r['job'],r['lease']) not in active] + paths = [] + for row in rows: + identifier = row['id'] + if len(identifier) != 32 or any(c not in '0123456789abcdef' for c in identifier): + raise ValueError('Invalid artifact identifier; cleanup stopped') + candidate = root / identifier + if candidate.is_symlink() or candidate.resolve().parent != root: + raise ValueError('Artifact path redirects outside storage') + if candidate.exists() and not candidate.is_file(): + raise ValueError('Artifact path is not a regular file') + paths.append(candidate) + report = {'mode':'apply' if apply else 'dry-run','obsoleteArtifacts':len(rows), + 'reservedBytes':sum(r['size'] for r in rows),'publishedArtifactsTouched':0} + if apply: + for candidate in paths: + candidate.unlink(missing_ok=True) + db.executemany('DELETE FROM artifacts WHERE id=?', [(r['id'],) for r in rows]) + return report + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--database', required=True, type=Path) + parser.add_argument('--apply', action='store_true') + parser.add_argument('--obsolete-artifacts', action='store_true', + help='Reclaim obsolete worker attempts instead of recovering audio uploads') + parser.add_argument('--gateway-stopped', action='store_true', + help='Confirm all gateway processes using this database have been stopped') + args = parser.parse_args() + if args.apply and not args.gateway_stopped: + parser.error('--apply requires --gateway-stopped; stop the service first') + operation = reclaim_artifacts if args.obsolete_artifacts else recover_uploads + print(json.dumps(operation(args.database, apply=args.apply), indent=2)) + + +if __name__ == '__main__': + main() diff --git a/src/beatforge/connector_revision.py b/src/beatforge/connector_revision.py new file mode 100644 index 0000000..d1a1fbf --- /dev/null +++ b/src/beatforge/connector_revision.py @@ -0,0 +1,68 @@ +"""Worker-side section editing using retained private analysis and full-map QA.""" +import json +import shutil +import sys +from pathlib import Path + +from beatforge.premium import SCRIPTS, PipelineCancelled +from beatforge.preview import chart_identity, chart_refs, confined, merge_section, read_json + + +def run_revision(root: Path, destination: Path, revision: dict, cancel_requested): + # The gateway supplies UUIDs and a content hash, never a filesystem path. + parent = root.resolve() / revision['parentJob'] + source = None + for attempt in sorted(parent.glob('*/map')): + if not attempt.resolve().is_relative_to(root.resolve()): + continue + if attempt.is_symlink() or any(p.is_symlink() or getattr(p, 'is_junction', lambda: False)() for p in attempt.rglob('*')): + continue + if chart_identity(attempt) == revision['baseHash']: + source = attempt + break + if source is None: + raise ValueError('The original worker cache is required for this revision') + if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + from choreography import generate_all + from timing_export import project_map_timing, project_plan_timing + from validate_map import validate_package + if cancel_requested(): + raise PipelineCancelled('Revision cancelled') + if validate_package(source).errors: + raise ValueError('The parent map no longer passes QA') + analysis = read_json(confined(source, '_beatforge/analysis.json')) + sections = read_json(confined(source, '_beatforge/sections.json')) + info = read_json(source / 'Info.dat') + ref = next(item for item in chart_refs(info) if item['_difficulty'] == revision['difficulty']) + original = read_json(confined(source, ref['_beatmapFilename'])) + if original.get('bpmEvents') or len(analysis.get('beatGrid', [])) < 2: + raise ValueError('Revision requires cached analysis and constant exported BPM') + if revision['endBeat'] * 60 / float(info['_beatsPerMinute']) > float(analysis['durationSeconds']) + 1e-6: + raise ValueError('Revision extends beyond the song') + merge_section(original, original, revision['startBeat'], revision['endBeat']) + maps, report = generate_all(analysis, sections, revision['seed'], + difficulties=[revision['difficulty']], mapping_plan=revision['mappingPlan']) + if cancel_requested(): + raise PipelineCancelled('Revision cancelled') + candidate = project_map_timing(maps[revision['difficulty']], analysis) + merged = merge_section(original, candidate, revision['startBeat'], revision['endBeat']) + shutil.copytree(source, destination) + confined(destination, ref['_beatmapFilename']).write_text(json.dumps(merged, indent=2), encoding='utf-8') + provenance_path = destination / '_beatforge/provenance.json' + provenance = read_json(provenance_path) if provenance_path.is_file() else {} + provenance.update(revision={**revision, 'preservedOutsideSelection': True}, + releaseGate={}, humanPlaytests=[], status='unreviewed_revision') + for key in ('reviews', 'providerReviews', 'playtests', 'vrPlaytestPassed', 'freshSightReadPassed'): + provenance.pop(key, None) + provenance_path.write_text(json.dumps(provenance, indent=2), encoding='utf-8') + plan_path = destination / '_beatforge/mapping_plan.json' + old_plan = read_json(plan_path) if plan_path.is_file() else {} + plan = project_plan_timing(report.get('mappingPlan', {'controls': revision['mappingPlan'], 'sections': sections.get('sections', [])}), analysis) + plan.update(appliesTo={key: revision[key] for key in ('difficulty', 'startBeat', 'endBeat')}, parentPlan=old_plan) + plan_path.write_text(json.dumps(plan, indent=2), encoding='utf-8') + qa = validate_package(destination) + (destination / '_beatforge/qa_report.json').write_text(json.dumps(qa.to_dict()), encoding='utf-8') + if cancel_requested(): + raise PipelineCancelled('Revision cancelled') + return {'status': 'invalid' if qa.errors else 'playtest_candidate'} diff --git a/src/beatforge/connector_store.py b/src/beatforge/connector_store.py new file mode 100644 index 0000000..50946bb --- /dev/null +++ b/src/beatforge/connector_store.py @@ -0,0 +1,189 @@ +"""Durable, owner-scoped connector jobs with expiring worker leases.""" +from __future__ import annotations + +import json +import secrets +import sqlite3 +import time +import uuid +from pathlib import Path +from contextlib import contextmanager + + +class ConnectorStore: + BLOB_BUDGET = 512 * 1024 * 1024 + def __init__(self, path: Path): + self.path = Path(path) + self.path.parent.mkdir(parents=True, exist_ok=True) + with self.connect() as db: + db.execute('''CREATE TABLE IF NOT EXISTS jobs ( + id TEXT PRIMARY KEY, owner TEXT NOT NULL, request_key TEXT NOT NULL, + payload TEXT NOT NULL, state TEXT NOT NULL DEFAULT 'queued', + attempts INTEGER NOT NULL DEFAULT 0, lease TEXT, lease_until REAL, + result TEXT, created REAL NOT NULL, updated REAL NOT NULL, + UNIQUE(owner, request_key))''') + db.execute('''CREATE TABLE IF NOT EXISTS uploads ( + id TEXT PRIMARY KEY, owner TEXT NOT NULL, filename TEXT NOT NULL, + size INTEGER NOT NULL, state TEXT NOT NULL DEFAULT 'pending', + sha256 TEXT, expires REAL NOT NULL)''') + db.execute('CREATE TABLE IF NOT EXISTS workers (owner TEXT PRIMARY KEY, seen REAL NOT NULL)') + + def worker_seen(self, owner: str): + with self.connect() as db: + db.execute('INSERT INTO workers(owner,seen) VALUES(?,?) ON CONFLICT(owner) DO UPDATE SET seen=excluded.seen', (owner, time.time())) + + def worker_online(self, owner: str) -> bool: + with self.connect() as db: + row = db.execute('SELECT seen FROM workers WHERE owner=?', (owner,)).fetchone() + return bool(row and row['seen'] > time.time() - 90) + + def check_lease(self, owner: str, job_id: str, token: str) -> dict: + with self.connect() as db: + row = db.execute("SELECT * FROM jobs WHERE id=? AND owner=? AND lease=? AND lease_until>? AND state='running'", (job_id, owner, token, time.time())).fetchone() + if row is None: + raise ValueError('Lease expired, revoked, or replaced') + return self.public(row) + + def reserve_upload(self, owner: str, filename: str, size: int) -> dict: + if not 1 <= size <= 64 * 1024 * 1024: + raise ValueError('Audio must be between 1 byte and 64 MiB') + with self.connect() as db: + db.execute('BEGIN IMMEDIATE') + # Pending reservations have no committed bytes. Serialize expiry with + # begin_upload so cleanup can never race an active or completed writer. + db.execute("DELETE FROM uploads WHERE state='pending' AND expires<=?", (time.time(),)) + usage = db.execute('SELECT COUNT(*) AS count, COALESCE(SUM(size),0) AS bytes FROM uploads WHERE owner=?', (owner,)).fetchone() + if usage['count'] >= 32 or usage['bytes'] + size > 256 * 1024 * 1024: + raise ValueError('Account upload quota reached') + self.check_blob_budget(db, size) + upload_id, expires = uuid.uuid4().hex, time.time() + 3600 + db.execute('INSERT INTO uploads(id,owner,filename,size,expires) VALUES(?,?,?,?,?)', (upload_id, owner, filename, size, expires)) + return self.get_upload(owner, upload_id) + + def get_upload(self, owner: str, upload_id: str) -> dict: + with self.connect() as db: + row = db.execute('SELECT * FROM uploads WHERE id=? AND owner=?', (upload_id, owner)).fetchone() + if row is None: + raise KeyError('Upload not found') + return {key: row[key] for key in ('id', 'filename', 'size', 'state', 'sha256', 'expires')} + + def begin_upload(self, owner: str, upload_id: str) -> dict: + with self.connect() as db: + changed = db.execute("UPDATE uploads SET state='writing' WHERE id=? AND owner=? AND state='pending' AND expires>?", (upload_id, owner, time.time())).rowcount + if not changed: + raise ValueError('Upload expired, already written, or unavailable') + return self.get_upload(owner, upload_id) + + def complete_upload(self, owner: str, upload_id: str, digest: str): + with self.connect() as db: + changed = db.execute("UPDATE uploads SET state='ready',sha256=? WHERE id=? AND owner=? AND state='writing' AND expires>?", (digest, upload_id, owner, time.time())).rowcount + if not changed: + raise ValueError('Upload expired or unavailable') + + def abort_upload(self, owner: str, upload_id: str): + with self.connect() as db: + db.execute("UPDATE uploads SET state='pending' WHERE id=? AND owner=? AND state='writing'", (upload_id, owner)) + + @contextmanager + def connect(self): + db = sqlite3.connect(self.path, timeout=15) + db.row_factory = sqlite3.Row + try: + page_size = db.execute('PRAGMA page_size').fetchone()[0] + db.execute(f'PRAGMA max_page_count={64 * 1024 * 1024 // page_size}') + with db: + yield db + finally: + db.close() + + def check_blob_budget(self, db, additional: int): + """Called inside the same write transaction as the reservation.""" + usage = db.execute('SELECT COALESCE(SUM(size),0) FROM uploads').fetchone()[0] + if db.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='artifacts'").fetchone(): + usage += db.execute('SELECT COALESCE(SUM(size),0) FROM artifacts').fetchone()[0] + if usage + additional > self.BLOB_BUDGET: + raise ValueError('Gateway storage capacity reached. Export data and contact the operator.') + + def submit(self, owner: str, request_key: str, payload: dict, *, max_active: int = 2) -> dict: + if not owner or not request_key: + raise ValueError('Owner and idempotency key are required') + encoded = json.dumps(payload, sort_keys=True, separators=(',', ':'), allow_nan=False) + now = time.time() + with self.connect() as db: + db.execute('BEGIN IMMEDIATE') + previous = db.execute('SELECT * FROM jobs WHERE owner=? AND request_key=?', (owner, request_key)).fetchone() + if previous: + if previous['payload'] != encoded: + raise ValueError('Idempotency key already belongs to a different request') + return self.public(previous) + active = db.execute("SELECT COUNT(*) FROM jobs WHERE owner=? AND state IN ('queued','running')", (owner,)).fetchone()[0] + if active >= max_active: + raise ValueError('Account active-job limit reached') + job_id = uuid.uuid4().hex + db.execute('INSERT INTO jobs(id,owner,request_key,payload,created,updated) VALUES(?,?,?,?,?,?)', + (job_id, owner, request_key, encoded, now, now)) + return self.get(owner, job_id) + + @staticmethod + def public(row) -> dict: + return {'id': row['id'], 'state': row['state'], 'attempts': row['attempts'], + 'request': json.loads(row['payload']), 'result': json.loads(row['result']) if row['result'] else None, + 'createdAt': row['created'], 'updatedAt': row['updated']} + + def get(self, owner: str, job_id: str) -> dict: + with self.connect() as db: + row = db.execute('SELECT * FROM jobs WHERE id=? AND owner=?', (job_id, owner)).fetchone() + if row is None: + raise KeyError('Job not found') + return self.public(row) + + def list_jobs(self, owner: str, limit: int = 20, offset: int = 0) -> dict: + if not 1 <= limit <= 100 or not 0 <= offset <= 100000: + raise ValueError('Use a limit of 1 to 100 and a nonnegative offset up to 100000') + with self.connect() as db: + rows = db.execute('SELECT * FROM jobs WHERE owner=? ORDER BY created DESC,id DESC LIMIT ? OFFSET ?', (owner, limit, offset)).fetchall() + total = db.execute('SELECT COUNT(*) FROM jobs WHERE owner=?', (owner,)).fetchone()[0] + return {'jobs': [self.public(row) for row in rows], 'total': total, 'offset': offset} + + def cancel(self, owner: str, job_id: str) -> dict: + with self.connect() as db: + changed = db.execute("UPDATE jobs SET state='cancelled',lease=NULL,lease_until=NULL,updated=? WHERE id=? AND owner=? AND state IN ('queued','running')", + (time.time(), job_id, owner)).rowcount + result = self.get(owner, job_id) + if not changed and result['state'] != 'cancelled': + raise ValueError('Completed jobs cannot be cancelled') + return result + + def claim(self, owner: str, *, lease_seconds: int = 60, max_attempts: int = 3) -> dict | None: + if lease_seconds < 1 or max_attempts < 1: + raise ValueError('Lease duration and attempt limit must be positive') + now = time.time() + with self.connect() as db: + db.execute('BEGIN IMMEDIATE') + db.execute("UPDATE jobs SET state='failed',lease=NULL,lease_until=NULL,updated=? WHERE owner=? AND state='running' AND lease_until<=? AND attempts>=?", (now, owner, now, max_attempts)) + row = db.execute("SELECT * FROM jobs WHERE owner=? AND attempts? AND state='running'", (now + lease_seconds, now, job_id, owner, token, now)).rowcount + if not changed: + raise ValueError('Lease expired, revoked, or replaced') + + def finish(self, owner: str, job_id: str, token: str, result: dict, *, failed: bool = False): + encoded = json.dumps(result, allow_nan=False) + now = time.time() + with self.connect() as db: + changed = db.execute("UPDATE jobs SET state=?,result=?,lease=NULL,lease_until=NULL,updated=? WHERE id=? AND owner=? AND lease=? AND lease_until>? AND state='running'", + ('failed' if failed else 'completed', encoded, now, job_id, owner, token, now)).rowcount + if not changed: + raise ValueError('Lease expired, revoked, or replaced') + return self.get(owner, job_id) diff --git a/src/beatforge/connector_uploads.py b/src/beatforge/connector_uploads.py new file mode 100644 index 0000000..930cd99 --- /dev/null +++ b/src/beatforge/connector_uploads.py @@ -0,0 +1,78 @@ +"""Bounded audio uploads. Client filenames never become server filesystem paths.""" +from __future__ import annotations + +import hashlib +from pathlib import Path + +import anyio + +from fastapi import HTTPException, Request +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from beatforge.connector_store import ConnectorStore + +UPLOAD_TIMEOUT_SECONDS = 300 + + +class UploadRequest(BaseModel): + model_config = ConfigDict(extra='forbid') + filename: str = Field(min_length=1, max_length=200) + size: int = Field(ge=1, le=64 * 1024 * 1024, strict=True) + + @field_validator('filename') + @classmethod + def audio_filename(cls, value: str) -> str: + if any(ord(char) < 32 or char in '/\\:' for char in value): + raise ValueError('Supply a filename, not a path') + if Path(value).suffix.lower() not in {'.wav', '.ogg', '.mp3', '.flac', '.m4a', '.mp4'}: + raise ValueError('Unsupported audio extension') + return value + + +class Uploads: + def __init__(self, store: ConnectorStore, root: Path): + self.store, self.root = store, Path(root).resolve() + self.root.mkdir(parents=True, exist_ok=True) + + def path(self, upload_id: str) -> Path: + # Only validated server identifiers can address a blob. + if len(upload_id) != 32 or any(char not in '0123456789abcdef' for char in upload_id): + raise KeyError('Upload not found') + path = (self.root / (upload_id + '.audio')).resolve() + if path.parent != self.root: + raise KeyError('Upload not found') + return path + + async def receive(self, owner: str, upload_id: str, request: Request) -> dict: + metadata = self.store.get_upload(owner, upload_id) + path = self.path(upload_id) + size = request.headers.get('content-length') + if size is not None and (not size.isdecimal() or int(size) != metadata['size']): + raise HTTPException(413, 'Upload size differs from its reservation') + self.store.begin_upload(owner, upload_id) + temporary = path.with_suffix('.part') + try: + digest, received = hashlib.sha256(), 0 + # Exclusive creation also refuses stale files after an interrupted process. + try: + with anyio.fail_after(UPLOAD_TIMEOUT_SECONDS): + with temporary.open('xb') as stream: + async for chunk in request.stream(): + received += len(chunk) + if received > metadata['size']: + raise HTTPException(413, 'Upload exceeds its reserved size') + stream.write(chunk) + digest.update(chunk) + except TimeoutError as error: + raise HTTPException(408, 'Audio upload timed out; retry the upload') from error + if received != metadata['size']: + raise HTTPException(422, 'Upload is incomplete') + temporary.replace(path) + self.store.complete_upload(owner, upload_id, digest.hexdigest()) + except BaseException: + # The immutable ready blob is never touched by a second upload request. + temporary.unlink(missing_ok=True) + path.unlink(missing_ok=True) + self.store.abort_upload(owner, upload_id) + raise + return self.store.get_upload(owner, upload_id) diff --git a/src/beatforge/connector_worker.py b/src/beatforge/connector_worker.py new file mode 100644 index 0000000..ec99005 --- /dev/null +++ b/src/beatforge/connector_worker.py @@ -0,0 +1,187 @@ +"""Outbound-only local generation worker. Never installs maps or launches the game.""" +from __future__ import annotations + +import argparse +import hashlib +import json +import logging +import os +import re +import threading +import time +from pathlib import Path +from urllib.parse import urlsplit + +import httpx + +from beatforge.connector_jobs import GenerationRequest +from beatforge.connector_export import prepare_artifacts +from beatforge.premium import PipelineCancelled, run_premium_pipeline + +log = logging.getLogger(__name__) + + +def transient_http(error: httpx.HTTPError) -> bool: + return isinstance(error, httpx.TransportError) or ( + isinstance(error, httpx.HTTPStatusError) and + (error.response.status_code in {408, 425, 429} or error.response.status_code >= 500)) + + +def validate_gateway(url: str) -> str: + parsed = urlsplit(url) + if (parsed.username or parsed.password or parsed.query or parsed.fragment or + parsed.path not in {'', '/'} or not parsed.hostname or + (parsed.scheme != 'https' and not (parsed.scheme == 'http' and parsed.hostname in {'127.0.0.1', 'localhost', '::1'}))): + raise ValueError('Use an HTTPS gateway origin, or HTTP on localhost for development') + return url.rstrip('/') + + +def run_once(client: httpx.Client, root: Path) -> bool: + response = client.post('/api/worker/claim') + response.raise_for_status() + job = response.json()['job'] + if not job: + return False + job_id = job['id'] + if not re.fullmatch('[a-f0-9]{32}', job_id): + raise ValueError('Invalid job identifier') + revision = job['request'].get('revision') + request = GenerationRequest.model_validate({**{key: value for key, value in job['request'].items() if key != 'revision'}, 'idempotencyKey': 'worker'}) + if revision: + from beatforge.connector_jobs import SectionRevision + if not re.fullmatch('[a-f0-9]{32}', revision.get('parentJob', '')): + raise ValueError('Invalid parent job identifier') + SectionRevision.model_validate({**{key: value for key, value in revision.items() if key != 'parentJob'}, 'idempotencyKey': 'worker'}) + lease_headers = {'X-BeatForge-Lease': job['leaseToken']} + # Each lease has a distinct output directory, including recovered attempts. + folder = root.resolve() / job_id / hashlib.sha256(job['leaseToken'].encode()).hexdigest()[:16] + folder.mkdir(parents=True, exist_ok=True) + cancelled, finished = threading.Event(), threading.Event() + + def heartbeat(): + while not finished.wait(15): + try: + reply = client.post(f'/api/worker/jobs/{job_id}/heartbeat', headers=lease_headers) + reply.raise_for_status() + except httpx.HTTPError: + # Stop expensive computation if lease ownership cannot be confirmed. + cancelled.set() + return + + thread = threading.Thread(target=heartbeat, daemon=True) + thread.start() + try: + if revision: + from beatforge.connector_revision import run_revision + result = run_revision(root, folder/'map', revision, cancelled.is_set) + else: + suffix = Path(job['upload']['filename']).suffix.lower() + if suffix not in {'.wav', '.ogg', '.mp3', '.flac', '.m4a', '.mp4'}: + raise ValueError('Unsupported audio type') + audio = folder / ('input' + suffix) + digest, size = hashlib.sha256(), 0 + with client.stream('GET', f'/api/worker/jobs/{job_id}/audio', headers=lease_headers) as reply: + reply.raise_for_status() + with audio.open('wb') as output: + for chunk in reply.iter_bytes(): + size += len(chunk) + if cancelled.is_set() or size > min(job['upload']['size'], 64*1024*1024): + raise PipelineCancelled('Download stopped') + output.write(chunk) + digest.update(chunk) + if size != job['upload']['size'] or digest.hexdigest() != job['upload']['sha256']: + raise ValueError('Audio integrity check failed') + result = run_premium_pipeline(audio=audio, output=folder/'map', title=request.title, + artist=request.artist, mapper=request.mapper, seed=request.seed, + anchors=None, palette=None, progress=lambda *args, **kwargs: None, + allow_unconfirmed=request.allowUnconfirmed, + difficulties=request.difficulties, mapping_plan=request.mappingPlan, + cancel_requested=cancelled.is_set) + (folder/'pipeline-result.json').write_text(json.dumps(result, indent=2), encoding='utf-8') + status = result['status'] + if status == 'invalid' and not (folder/'map/_beatforge/qa_report.json').is_file(): + status = 'error' + if status == 'playtest_candidate': + outputs = prepare_artifacts(folder/'map', folder/'publish', request.mappingPlan) + for name, path in outputs.items(): + if cancelled.is_set(): + raise PipelineCancelled('Lease lost before artifact upload') + with path.open('rb') as content: + reply = client.put(f'/api/worker/jobs/{job_id}/artifacts/{name}', + headers={**lease_headers, 'Content-Length': str(path.stat().st_size), + 'Content-Type': 'application/octet-stream'}, content=content) + reply.raise_for_status() + message = {'playtest_candidate': 'Map and review artifacts are ready. Human playtesting is still required.', + 'needs_anchors': 'Timing anchors are required before generation can continue.', + 'needs_palette': 'The mapping pipeline requires palette approval.', + 'corpus_incomplete': 'The worker needs its official mapping corpus configured.', + 'invalid': 'The generated map did not pass structural validation.', + 'error': 'The worker could not generate this map. Inspect its local output.'}[status] + reply = client.post(f'/api/worker/jobs/{job_id}/finish', headers=lease_headers, + json={'status': status, 'message': message, 'humanPlaytestRequired': True}) + reply.raise_for_status() + except PipelineCancelled: + # A lost/cancelled lease must not publish a late result. + pass + except Exception as error: + if isinstance(error, httpx.HTTPError) and transient_http(error): + # A timed-out finish might already have committed. Do not replace a + # successful result with an error, or consume retryable work. The + # gateway either retains completion or reclaims the expired lease. + raise + if not cancelled.is_set(): + reply = client.post(f'/api/worker/jobs/{job_id}/finish', headers=lease_headers, + json={'status': 'error', 'message': 'Worker failed. Check local setup and audio format.', + 'humanPlaytestRequired': True}) + reply.raise_for_status() + raise + finally: + finished.set() + thread.join(timeout=20) + return True + + +def serve(client: httpx.Client, root: Path, *, once: bool = False, sleep=time.sleep): + """Keep polling after transient outages; terminal auth/configuration errors stop.""" + failures = 0 + while True: + try: + worked = run_once(client, root) + failures = 0 + except httpx.HTTPError as error: + if once or not transient_http(error): + raise + failures += 1 + delay = min(60, 2 ** min(failures, 6)) + # Exception text can contain private URLs. Emit only safe status. + status = error.response.status_code if isinstance(error, httpx.HTTPStatusError) else 'network' + log.warning('Gateway unavailable (%s); reconnecting in %s seconds', status, delay) + sleep(delay) + continue + except Exception: + if once: + raise + log.error('Job failed; inspect local worker output. Continuing in 5 seconds.') + sleep(5) + continue + if once: + return + if not worked: + sleep(5) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--gateway', required=True) + parser.add_argument('--work-dir', type=Path, default=Path('data/worker')) + parser.add_argument('--once', action='store_true') + args = parser.parse_args() + gateway = validate_gateway(args.gateway) + token = os.environ['BEATFORGE_WORKER_TOKEN'] + with httpx.Client(base_url=gateway, headers={'Authorization': f'Bearer {token}'}, timeout=20, + follow_redirects=False) as client: + serve(client, args.work_dir, once=args.once) + + +if __name__ == '__main__': + main() diff --git a/src/beatforge/editorial_models.py b/src/beatforge/editorial_models.py new file mode 100644 index 0000000..0f8a89f --- /dev/null +++ b/src/beatforge/editorial_models.py @@ -0,0 +1,67 @@ +"""Shared editorial contracts without local Studio or audio dependencies.""" +from __future__ import annotations + +from typing import Annotated, Any, Literal +from pydantic import BaseModel, ConfigDict, Field, StringConstraints, model_validator + +Difficulty = Literal["Easy", "Normal", "Hard", "Expert", "ExpertPlus"] +FeedbackTag = Literal["too_dense", "too_sparse", "awkward", "tiring", "repetitive", "off_beat", "good_flow"] +Tester = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=120)] +Rating = Annotated[int, Field(strict=True, ge=1, le=5)] + + +class Request(BaseModel): + model_config = ConfigDict(extra="forbid", allow_inf_nan=False) + + +class Ratings(Request): + overall: Rating | None = None + flow: Rating | None = None + readability: Rating | None = None + musicality: Rating | None = None + variety: Rating | None = None + + +class FeedbackRequest(Request): + jobId: str = Field(min_length=1, max_length=80) + difficulty: Difficulty + tester: Tester = "local" + ratings: Ratings = Field(default_factory=Ratings) + tags: list[FeedbackTag] = Field(default_factory=list, max_length=7) + notes: str = Field(default="", max_length=2000) + startBeat: float | None = Field(default=None, ge=0) + endBeat: float | None = Field(default=None, gt=0) + + @model_validator(mode="after") + def meaningful_feedback(self): + if (self.startBeat is None) != (self.endBeat is None): + raise ValueError("Provide both startBeat and endBeat for passage feedback") + if self.startBeat is not None and self.endBeat <= self.startBeat: + raise ValueError("endBeat must follow startBeat") + if not self.ratings.model_dump(exclude_none=True) and not self.tags and not self.notes.strip(): + raise ValueError("Add a rating, tag, or note") + return self + + +class ComparisonRequest(Request): + preferredJob: str = Field(min_length=1, max_length=80) + alternateJob: str = Field(min_length=1, max_length=80) + difficulty: Difficulty + tester: Tester = "local" + notes: str = Field(default="", max_length=2000) + + +class PresetRequest(Request): + name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=80)] + mappingPlan: dict[str, Any] + + +class SuggestionsRequest(Request): + mappingPlan: dict[str, Any] = Field(default_factory=dict) + tester: Tester = "local" + difficulty: Difficulty | None = None + + +class AcceptanceRequest(Request): + tester: Tester = "local" + notes: str = Field(default="", max_length=2000) diff --git a/src/beatforge/feedback_guidance.py b/src/beatforge/feedback_guidance.py new file mode 100644 index 0000000..dcafdc7 --- /dev/null +++ b/src/beatforge/feedback_guidance.py @@ -0,0 +1,120 @@ +"""Bounded, opt-in creative suggestions from distinct human playtest reports. + +These helpers do not train a model, verify a headset session, or change a plan. +The caller persists human reports and asks the user to apply suggestedPlan. +""" + +from __future__ import annotations + +from collections import defaultdict +from datetime import datetime, timezone +import math +from typing import Any + +from .mapping_plan import normalize_mapping_plan + + +def _timestamp(value: Any) -> float: + if not isinstance(value, str): + return 0.0 + try: + moment = datetime.fromisoformat(value.replace("Z", "+00:00")) + if moment.tzinfo is None: + moment = moment.replace(tzinfo=timezone.utc) + return moment.timestamp() + except (ValueError, OverflowError): + return 0.0 + + +def _section_key(record: dict[str, Any]) -> tuple[float | None, float | None] | None: + start, end = record.get("startBeat"), record.get("endBeat") + if start is None and end is None: + return (None, None) + if any(isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) for value in (start, end)): + return None + if start < 0 or end <= start: + return None + return (float(start), float(end)) + + +def derive_feedback_adjustment( + records: list[dict[str, Any]], current_plan: dict[str, Any] | None, + *, tester: str = "local", difficulty: str | None = None, +) -> dict[str, Any]: + """One vote per distinct chart, using each chart/section's latest human report.""" + plan = normalize_mapping_plan(current_plan) + latest: dict[tuple[Any, ...], dict[str, Any]] = {} + for record in records: + if not isinstance(record, dict) or record.get("source") != "human" or record.get("schemaVersion", 1) != 1: + continue + if record.get("tester", "local") != tester or (difficulty is not None and record.get("difficulty") != difficulty): + continue + chart_hash = record.get("chartHash") + section = _section_key(record) + if not isinstance(chart_hash, str) or not chart_hash or section is None: + continue + key = (chart_hash, tester, *section) + if key not in latest or _timestamp(record.get("createdAt")) >= _timestamp(latest[key].get("createdAt")): + latest[key] = record + by_chart: dict[str, list[dict[str, Any]]] = defaultdict(list) + for record in latest.values(): + by_chart[record["chartHash"]].append(record) + count = len(by_chart) + result: dict[str, Any] = {"suggestedPlan": plan, "evidenceCount": count, "reasons": [], "ready": count >= 3, "changedFields": {}, "nextActions": []} + if count < 3: + result["reasons"] = [f"Need reports for at least 3 distinct charts from {tester}; found {count}."] + return result + + votes: dict[str, int] = defaultdict(int) + for reports in by_chart.values(): + tags = {tag for report in reports for tag in (report.get("tags") or []) if isinstance(tag, str)} + dense = bool(tags & {"too_dense", "tiring"}) + sparse = "too_sparse" in tags + # Conflicting reports within a chart cancel instead of multiplying votes. + votes["dense"] += int(dense and not sparse) + votes["sparse"] += int(sparse and not dense) + votes["repetitive"] += int("repetitive" in tags) + votes["off_beat"] += int("off_beat" in tags) + readability = [] + for report in reports: + ratings = report.get("ratings", {}) + rating = ratings.get("readability") if isinstance(ratings, dict) else None + if isinstance(rating, (int, float)) and not isinstance(rating, bool) and math.isfinite(rating) and 1 <= rating <= 5: + readability.append(float(rating)) + votes["awkward"] += int("awkward" in tags or bool(readability and sum(readability) / len(readability) <= 2)) + + suggestion = dict(plan) + reasons: list[str] = [] + density_signal = (votes["dense"] - votes["sparse"]) / count + if abs(density_signal) >= 1 / 3 and max(votes["dense"], votes["sparse"]) >= 2: + suggestion["density"] = round(max(0.5, min(1.5, plan["density"] * (1.0 - 0.15 * density_signal))), 4) + direction = "Lower" if density_signal > 0 else "Raise" + reasons.append(f"{direction} density slightly: {votes['dense']} charts were reported too dense or tiring and {votes['sparse']} too sparse.") + if votes["awkward"] >= 2 and votes["awkward"] / count >= 0.5: + suggestion["intensity"] = round(max(0.5, plan["intensity"] * 0.9), 4) + reasons.append(f"Try 10% lower intensity: {votes['awkward']} of {count} charts had awkward movement or low reported readability.") + if votes["repetitive"] >= 2 and votes["repetitive"] / count >= 0.5 and plan["candidateCount"] < 3: + suggestion["candidateCount"] = plan["candidateCount"] + 1 + reasons.append(f"Compare one more safe candidate: {votes['repetitive']} of {count} charts were reported repetitive.") + if votes["off_beat"]: + result["nextActions"].append("Review the sample timing and click track before generating again; density changes cannot repair off-beat notes.") + result["suggestedPlan"] = normalize_mapping_plan(suggestion) + result["changedFields"] = {key: {"from": plan[key], "to": suggestion[key]} for key in ("density", "intensity", "candidateCount") if plan[key] != suggestion[key]} + result["reasons"] = reasons or ["These reports do not establish a consistent creative-control adjustment."] + return result + + +def qa_next_actions(qa: dict[str, Any] | None, timing: dict[str, Any] | None = None) -> list[dict[str, str]]: + """Order concrete next checks from existing machine reports; never certify playability.""" + qa = qa or {} + timing = timing or {} + actions = [] + errors = qa.get("errors") or [] + if errors: + first = errors[0] if isinstance(errors[0], dict) else {} + actions.append({"priority": "blocking", "action": "repair_structure", "detail": str(first.get("message") or first.get("code") or "Resolve the structural validation errors before playtesting.")}) + if timing and timing.get("status") != "timing_verified": + actions.append({"priority": "review", "action": "review_timing", "detail": "Check the click track at the start, middle, and end, then confirm timing anchors."}) + if not errors: + actions.append({"priority": "playtest", "action": "human_playtest", "detail": "Play the chart slowly, then at full speed, and record flow and readability feedback."}) + return actions diff --git a/src/beatforge/learning.py b/src/beatforge/learning.py new file mode 100644 index 0000000..4161e2b --- /dev/null +++ b/src/beatforge/learning.py @@ -0,0 +1,203 @@ +"""Local, explicit human feedback and preferences; no automatic model training.""" + +from __future__ import annotations + +import hashlib +import json +import math +import sys +import threading +import uuid +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from fastapi import APIRouter, HTTPException + +from beatforge.feedback_guidance import derive_feedback_adjustment +from beatforge.mapping_plan import normalize_mapping_plan +from beatforge.premium import ROOT, SCRIPTS +from beatforge.preview import chart_identity, chart_refs, confined, read_json + +router = APIRouter() +LEARNING_FILE = ROOT / "data" / "learning.json" +LEARNING_LOCK = threading.RLock() +from beatforge.editorial_models import ( + Difficulty, FeedbackTag, Tester, Rating, Request, Ratings, FeedbackRequest, + ComparisonRequest, PresetRequest, SuggestionsRequest, AcceptanceRequest, +) + + +def _now() -> str: + return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _empty() -> dict[str, Any]: + return {"schemaVersion": 1, "feedback": [], "comparisons": [], "presets": [], "acceptedRevisions": []} + + +def _read_store() -> dict[str, Any]: + if not LEARNING_FILE.is_file(): + return _empty() + try: + data = json.loads(LEARNING_FILE.read_text(encoding="utf-8")) + if not isinstance(data, dict) or data.get("schemaVersion") != 1: + raise ValueError("unsupported learning schema") + for key in ("feedback", "comparisons", "presets", "acceptedRevisions"): + if not isinstance(data.get(key), list) or any(not isinstance(item, dict) for item in data[key]): + raise ValueError("invalid learning records") + return data + except (OSError, ValueError) as error: + raise HTTPException(503, "Saved learning data is unreadable or unsupported. The original file has been preserved.") from error + + +def _write_store(data: dict[str, Any]) -> None: + LEARNING_FILE.parent.mkdir(parents=True, exist_ok=True) + temporary = LEARNING_FILE.with_name(f"learning.{uuid.uuid4().hex}.tmp") + try: + with temporary.open("w", encoding="utf-8") as handle: + json.dump(data, handle, indent=2, allow_nan=False) + handle.write("\n") + handle.flush() + import os + os.fsync(handle.fileno()) + temporary.replace(LEARNING_FILE) + finally: + temporary.unlink(missing_ok=True) + + +def _plan(value: dict[str, Any] | None) -> dict[str, Any]: + try: + return normalize_mapping_plan(value) + except (TypeError, ValueError) as error: + raise HTTPException(422, f"Invalid mapping plan: {error}") from error + + +def _artifact(job_id: str, difficulty: str) -> tuple[dict[str, Any], dict[str, Any]]: + """Bind evidence to disk contents, including playback settings for this chart.""" + from beatforge import api + import soundfile as sf + + status = api._read_status(job_id) + if status.get("status") in api.ACTIVE_STATES: + raise HTTPException(409, "Wait for this map to finish before recording feedback") + folder = api._job_dir(job_id) / "map" + info = read_json(confined(folder, "Info.dat")) + entry = next((item for item in chart_refs(info) if item.get("_difficulty") == difficulty), None) + if entry is None: + raise HTTPException(422, "This difficulty is not present in the selected map") + chart = confined(folder, str(entry["_beatmapFilename"])) + chart_payload = read_json(chart) + audio = confined(folder, str(info.get("_songFilename", ""))) + playback = {"bpm": info.get("_beatsPerMinute"), "offset": info.get("_songTimeOffset", 0), "difficulty": difficulty, "njs": entry.get("_noteJumpMovementSpeed"), "spawnOffset": entry.get("_noteJumpStartBeatOffset")} + digest = hashlib.sha256() + try: + digest.update(json.dumps(playback, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()) + digest.update(json.dumps(chart_payload, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()) + except (TypeError, ValueError) as error: + raise HTTPException(422, "The selected chart contains invalid playback data") from error + audio_digest = hashlib.sha256() + with audio.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + audio_digest.update(chunk) + try: + duration = sf.info(audio).duration + bpm = float(info.get("_beatsPerMinute", 120)) + if not math.isfinite(bpm) or bpm <= 0: + raise ValueError("Invalid tempo") + except (OSError, RuntimeError, TypeError, ValueError) as error: + raise HTTPException(422, "The selected map has unreadable audio or invalid tempo") from error + snapshot = {"jobId": job_id, "difficulty": difficulty, "chartHash": digest.hexdigest(), "mapHash": chart_identity(folder), "audioHash": audio_digest.hexdigest(), "durationBeats": duration * bpm / 60, "mappingPlan": _plan(status.get("mappingPlan")), "title": str(info.get("_songName", "Untitled")), "engine": str(status.get("engine") or "unknown")} + return snapshot, status + + +@router.get("/api/learning") +def learning_summary() -> dict[str, Any]: + with LEARNING_LOCK: + data = _read_store() + if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + from rl.registry import registry_status + + return {"schemaVersion": 1, "feedbackCount": len(data["feedback"]), "comparisonCount": len(data["comparisons"]), "acceptedRevisionCount": len(data["acceptedRevisions"]), "presets": data["presets"], "recentFeedback": data["feedback"][-20:][::-1], "suggestions": derive_feedback_adjustment(data["feedback"], {}, tester="local"), "modelRegistry": registry_status(LEARNING_FILE.parent / "model_registry")} + + +@router.post("/api/learning/suggestions") +def learning_suggestions(request: SuggestionsRequest) -> dict[str, Any]: + plan = _plan(request.mappingPlan) + with LEARNING_LOCK: + data = _read_store() + return derive_feedback_adjustment(data["feedback"], plan, tester=request.tester, difficulty=request.difficulty) + + +@router.post("/api/feedback") +def record_feedback(request: FeedbackRequest) -> dict[str, Any]: + snapshot, _ = _artifact(request.jobId, request.difficulty) + if request.endBeat is not None and request.endBeat > snapshot["durationBeats"] + 1e-6: + raise HTTPException(422, "The feedback passage extends beyond the song") + record = {**snapshot, "schemaVersion": 1, "id": uuid.uuid4().hex[:12], "source": "human", "createdAt": _now(), "tester": request.tester, "ratings": request.ratings.model_dump(exclude_none=True), "tags": sorted(set(request.tags)), "notes": request.notes, "startBeat": request.startBeat, "endBeat": request.endBeat} + with LEARNING_LOCK: + data = _read_store() + data["feedback"].append(record) + _write_store(data) + return {"feedback": record, "feedbackCount": len(data["feedback"])} + + +@router.post("/api/comparisons") +def record_comparison(request: ComparisonRequest) -> dict[str, Any]: + preferred, _ = _artifact(request.preferredJob, request.difficulty) + alternate, _ = _artifact(request.alternateJob, request.difficulty) + if preferred["audioHash"] != alternate["audioHash"]: + raise HTTPException(409, "Compare maps made from the exact same audio file") + if preferred["chartHash"] == alternate["chartHash"]: + raise HTTPException(409, "The selected difficulty charts are identical") + pair = sorted((preferred["chartHash"], alternate["chartHash"])) + identity = hashlib.sha256(json.dumps([preferred["audioHash"], request.difficulty, request.tester.casefold(), pair]).encode()).hexdigest() + record = {"schemaVersion": 1, "id": identity[:20], "source": "human", "createdAt": _now(), "tester": request.tester, "difficulty": request.difficulty, "audioHash": preferred["audioHash"], "chartPair": pair, "preferredJob": request.preferredJob, "alternateJob": request.alternateJob, "preferredChartHash": preferred["chartHash"], "alternateChartHash": alternate["chartHash"], "preferredMappingPlan": preferred["mappingPlan"], "alternateMappingPlan": alternate["mappingPlan"], "notes": request.notes} + with LEARNING_LOCK: + data = _read_store() + replaced = any(item.get("id") == record["id"] for item in data["comparisons"]) + data["comparisons"] = [item for item in data["comparisons"] if item.get("id") != record["id"]] + [record] + _write_store(data) + return {"comparison": record, "comparisonCount": len(data["comparisons"]), "replaced": replaced} + + +@router.post("/api/presets") +def save_preset(request: PresetRequest) -> dict[str, Any]: + preset = {"id": uuid.uuid4().hex[:12], "name": request.name, "mappingPlan": _plan(request.mappingPlan), "createdAt": _now()} + with LEARNING_LOCK: + data = _read_store() + data["presets"].append(preset) + _write_store(data) + return preset + + +def record_accepted_revision(job_id: str, tester: str = "local", notes: str = "") -> dict[str, Any]: + from beatforge import api + + request = AcceptanceRequest(tester=tester, notes=notes) + status = api._read_status(job_id) + revision = status.get("revision") + if not status.get("revisionOf") or not isinstance(revision, dict): + raise HTTPException(409, "Only a generated section revision can be accepted") + if status.get("localStatus") not in {"playtest_candidate", "unconfirmed_pack"} or api._qa_report(api._job_dir(job_id) / "map")["errors"]: + raise HTTPException(409, "Resolve the revision's structural validation failures first") + snapshot, _ = _artifact(job_id, str(revision.get("difficulty", ""))) + record = {**snapshot, "schemaVersion": 1, "id": uuid.uuid4().hex[:12], "source": "human", "createdAt": _now(), "tester": request.tester, "notes": request.notes, "parentJob": status["revisionOf"], "revision": {key: revision.get(key) for key in ("difficulty", "startBeat", "endBeat", "seed", "baseHash")}, "acceptance": "preferred_section_revision"} + with LEARNING_LOCK: + data = _read_store() + data["acceptedRevisions"] = [item for item in data["acceptedRevisions"] if not (item.get("chartHash") == record["chartHash"] and item.get("tester", "").casefold() == request.tester.casefold())] + [record] + _write_store(data) + return record + + +@router.post("/api/jobs/{job_id}/accept-revision") +def accept_revision(job_id: str, request: AcceptanceRequest) -> dict[str, Any]: + return {"acceptedRevision": record_accepted_revision(job_id, request.tester, request.notes)} + + +@router.get("/api/learning/export") +def export_learning() -> dict[str, Any]: + with LEARNING_LOCK: + data = _read_store() + return {"schemaVersion": 1, "kind": "beatforge-human-learning", "exportedAt": _now(), "containsAudio": False, "containsOfficialCorpus": False, "feedback": data["feedback"], "preferences": data["comparisons"], "presets": data["presets"], "acceptedRevisions": data["acceptedRevisions"]} diff --git a/src/beatforge/mapping_plan.py b/src/beatforge/mapping_plan.py new file mode 100644 index 0000000..048dbf1 --- /dev/null +++ b/src/beatforge/mapping_plan.py @@ -0,0 +1,13 @@ +"""Studio access to the portable mapper's single creative-plan contract.""" + +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + +_path = Path(__file__).resolve().parents[2] / "skills" / "beat-saber-mapping" / "scripts" / "mapping_plan.py" +_spec = spec_from_file_location("_beatforge_mapping_plan", _path) +if _spec is None or _spec.loader is None: + raise ImportError("BeatForge mapping plan module is missing") +_module = module_from_spec(_spec) +_spec.loader.exec_module(_module) +normalize_mapping_plan = _module.normalize_mapping_plan +build_section_plan = _module.build_section_plan diff --git a/src/beatforge/premium.py b/src/beatforge/premium.py index 00df110..d7d9a19 100644 --- a/src/beatforge/premium.py +++ b/src/beatforge/premium.py @@ -4,6 +4,7 @@ import json import os +import signal import subprocess import sys import threading @@ -11,9 +12,35 @@ from pathlib import Path from typing import Any, Callable +from .mapping_plan import normalize_mapping_plan + PROGRESS_PREFIX = "BEATFORGE_PROGRESS\t" +class PipelineCancelled(RuntimeError): + """The Studio job cancelled its mapper and child analysis processes.""" + + +def _stop_process_tree(process: subprocess.Popen) -> None: + if process.poll() is not None: + return + if os.name == "nt": + subprocess.run(["taskkill", "/PID", str(process.pid), "/T", "/F"], capture_output=True, check=False) + else: + try: + os.killpg(process.pid, signal.SIGTERM) + except ProcessLookupError: + pass + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + if os.name != "nt": + os.killpg(process.pid, signal.SIGKILL) + else: + process.kill() + process.wait() + + def _notify_progress(progress: Callable[..., None], stage: str, detail: str, percent: float | None = None) -> None: kwargs: dict[str, Any] = {} if percent is not None: @@ -57,7 +84,18 @@ def run_premium_pipeline( cover: Path | None = None, allow_unconfirmed: bool = False, difficulties: list[str] | None = None, + mapping_plan: dict[str, Any] | None = None, + engine: str = "premium", + rl_model: Path | None = None, + cancel_requested: Callable[[], bool] | None = None, + analysis_dir: Path | None = None, ) -> dict[str, Any]: + if engine not in {"premium", "rl"}: + raise ValueError("engine must be premium or rl") + if engine == "rl" and (rl_model is None or not Path(rl_model).is_file()): + raise ValueError("The RL engine requires an existing trained checkpoint") + if cancel_requested and cancel_requested(): + raise PipelineCancelled("Mapping cancelled before the pipeline started") command = [ sys.executable, str(SCRIPTS / "generate_map.py"), @@ -88,8 +126,18 @@ def run_premium_pipeline( command += ["--continue-unconfirmed"] for name in difficulties or []: command += ["--difficulty", name] + if mapping_plan is not None: + plan = normalize_mapping_plan(mapping_plan) + plan_path = output / "_beatforge" / "mapping_request.json" + plan_path.parent.mkdir(parents=True, exist_ok=True) + plan_path.write_text(json.dumps(plan, indent=2), encoding="utf-8") + command += ["--mapping-plan", str(plan_path)] + if engine == "rl": + command += ["--engine", "rl", "--rl-model", str(Path(rl_model).resolve())] + if analysis_dir is not None: + command += ["--analysis-dir", str(analysis_dir)] progress("track", f"{title} · {artist}") - progress("pipeline", "starting official-premium generate_map") + progress("pipeline", f"starting {engine} generate_map") environment = os.environ.copy() environment["PYTHONUNBUFFERED"] = "1" process = subprocess.Popen( @@ -102,9 +150,11 @@ def run_premium_pipeline( errors="replace", env=environment, bufsize=1, + start_new_session=os.name != "nt", ) stdout_chunks: list[str] = [] stderr_chunks: list[str] = [] + progress_errors: list[Exception] = [] def consume_stderr() -> None: assert process.stderr is not None @@ -120,7 +170,10 @@ def consume_stderr() -> None: percent = float(parts[3]) except ValueError: percent = None - _notify_progress(progress, parts[1], parts[2], percent) + try: + _notify_progress(progress, parts[1], parts[2], percent) + except Exception as error: + progress_errors.append(error) def consume_stdout() -> None: assert process.stdout is not None @@ -131,9 +184,26 @@ def consume_stdout() -> None: stdout_thread = threading.Thread(target=consume_stdout, daemon=True) stderr_thread.start() stdout_thread.start() - returncode = process.wait() - stderr_thread.join() - stdout_thread.join() + try: + while True: + if progress_errors: + raise progress_errors[0] + if cancel_requested and cancel_requested(): + _stop_process_tree(process) + raise PipelineCancelled("Mapping cancelled") + try: + returncode = process.wait(timeout=0.25) + break + except subprocess.TimeoutExpired: + continue + except BaseException: + _stop_process_tree(process) + raise + finally: + stderr_thread.join() + stdout_thread.join() + if progress_errors: + raise progress_errors[0] stdout_text = "".join(stdout_chunks) stderr_text = "".join(stderr_chunks) status = { diff --git a/src/beatforge/preview.py b/src/beatforge/preview.py new file mode 100644 index 0000000..3cd28f9 --- /dev/null +++ b/src/beatforge/preview.py @@ -0,0 +1,341 @@ +"""Read chart previews and create isolated, fully validated section revisions.""" + +from __future__ import annotations + +import copy +import hashlib +import json +import math +import shutil +import sys +import time +import uuid +from pathlib import Path +from typing import Any + +from fastapi import APIRouter, BackgroundTasks, HTTPException, Query +from fastapi.responses import FileResponse +from pydantic import BaseModel, ConfigDict, Field + +from beatforge.premium import ROOT, SCRIPTS, package_map, summarize_map + +router = APIRouter() +COLLECTIONS = ("colorNotes", "bombNotes", "obstacles", "sliders", "burstSliders", "basicBeatmapEvents", "colorBoostBeatmapEvents", "lightColorEventBoxGroups", "lightRotationEventBoxGroups", "lightTranslationEventBoxGroups", "vfxEventBoxGroups") + + +@router.post("/api/mapping-plan/resolve") +def resolve_mapping_plan(payload: dict[str, Any]) -> dict[str, Any]: + from beatforge.mapping_plan import normalize_mapping_plan + try: + return normalize_mapping_plan(payload) + except ValueError as error: + raise HTTPException(422, str(error)) from error + + +def confined(root: Path, relative: str) -> Path: + path = (root / relative).resolve() + if not path.is_relative_to(root.resolve()) or not path.is_file(): + raise HTTPException(404, "Referenced map file is missing or outside the map folder") + return path + + +def read_json(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8-sig")) + if not isinstance(value, dict): + raise ValueError("expected an object") + return value + except (OSError, ValueError) as error: + raise HTTPException(422, f"Cannot read {path.name}: {error}") from error + + +def chart_refs(info: dict[str, Any]) -> list[dict[str, Any]]: + return [entry for group in info.get("_difficultyBeatmapSets", []) if group.get("_beatmapCharacteristicName") == "Standard" for entry in group.get("_difficultyBeatmaps", [])] + + +def chart_identity(folder: Path) -> str: + info = read_json(confined(folder, "Info.dat")) + names = ["Info.dat", str(info.get("_songFilename", "song.ogg"))] + [str(entry["_beatmapFilename"]) for entry in chart_refs(info)] + digest = hashlib.sha256() + for name in sorted(set(names)): + digest.update(name.encode()) + with confined(folder, name).open('rb') as stream: + for chunk in iter(lambda: stream.read(1048576), b''): + digest.update(chunk) + return digest.hexdigest() + + +def _map(job_id: str) -> tuple[dict[str, Any], Path]: + from beatforge import api + status = api._read_status(job_id) + folder = api._job_dir(job_id) / "map" + if not (folder / "Info.dat").is_file(): + raise HTTPException(404, "This run has no chart to preview yet") + return status, folder + + +def waveform(path: Path, bins: int = 1200) -> tuple[float, list[float]]: + """Read bounded blocks, including multichannel audio, without loading a song in RAM.""" + import numpy as np + import soundfile as sf + with sf.SoundFile(path) as audio: + duration = len(audio) / audio.samplerate + width = max(1, math.ceil(len(audio) / bins)) + peaks: list[float] = [] + while len(peaks) < bins: + block = audio.read(width, dtype="float32", always_2d=True) + if not len(block): + break + peaks.append(round(float(np.max(np.abs(block))), 4)) + return duration, peaks + + +def preview_payload(folder: Path, difficulty: str | None = None) -> dict[str, Any]: + if not (folder / "_beatforge").resolve().is_relative_to(folder.resolve()): + raise HTTPException(404, "Map metadata is outside the map folder") + info = read_json(confined(folder, "Info.dat")) + refs = chart_refs(info) + if not refs: + raise HTTPException(422, "The preview currently supports Standard charts with v2 Info and v3 gameplay") + selected = next((ref for ref in refs if ref.get("_difficulty") == difficulty), None) if difficulty else refs[0] + if not selected: + raise HTTPException(404, "Difficulty is not present in this map") + filename = str(selected["_beatmapFilename"]) + chart = read_json(confined(folder, filename)) + if not str(chart.get("version", "")).startswith("3."): + raise HTTPException(422, "The preview supports v3 gameplay. This chart's format is preserved unchanged.") + audio_path = confined(folder, str(info.get("_songFilename", "song.ogg"))) + with audio_path.open('rb') as stream: + audio_hash = hashlib.file_digest(stream, 'sha256').hexdigest() + cache = folder / "_beatforge" / "preview-waveform.json" + if not cache.resolve().is_relative_to(folder.resolve()): + raise HTTPException(404, "Waveform cache is outside the map folder") + cached: dict[str, Any] = {} + if cache.is_file(): + try: + cached = read_json(cache) + except HTTPException: + pass + cache_duration = cached.get("duration") + cache_peaks = cached.get("waveform") + valid_cache = ( + isinstance(cache_duration, (int, float)) and math.isfinite(cache_duration) and cache_duration > 0 + and isinstance(cache_peaks, list) and 0 < len(cache_peaks) <= 1200 + and all(isinstance(value, (int, float)) and math.isfinite(value) and 0 <= value <= 1 for value in cache_peaks) + ) + if cached.get("audioHash") != audio_hash or not valid_cache: + duration, peaks = waveform(audio_path) + cached = {"audioHash": audio_hash, "duration": duration, "waveform": peaks} + cache.parent.mkdir(parents=True, exist_ok=True) + cache.write_text(json.dumps(cached), encoding="utf-8") + metadata: dict[str, Any] = {} + for key, name in (("analysis", "analysis.json"), ("sections", "sections.json"), ("qa", "qa_report.json"), ("provenance", "provenance.json"), ("mappingPlan", "mapping_plan.json")): + candidate = folder / "_beatforge" / name + metadata[key] = read_json(confined(folder, str(candidate.relative_to(folder)))) if candidate.is_file() else {} + bpm = float(info.get("_beatsPerMinute", 120)) + if not math.isfinite(bpm) or bpm <= 0: + raise HTTPException(422, "Invalid chart tempo") + color_schemes = info.get("_colorSchemes") or [] + scheme_index = int(selected.get("_beatmapColorSchemeIdx", -1)) + scheme = color_schemes[scheme_index].get("colorScheme", {}) if 0 <= scheme_index < len(color_schemes) else {} + if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + from timing_export import project_plan_timing + section_plan = metadata["mappingPlan"] + if not section_plan.get("sections"): + section_plan = project_plan_timing(metadata["sections"], metadata["analysis"]) if metadata["analysis"].get("beatGrid") else metadata["sections"] + qa = metadata["qa"] + findings = [item for item in qa.get("errors", []) + qa.get("warnings", []) if item.get("file") in (None, filename) or item.get("difficulty") == selected["_difficulty"]] + return { + "schemaVersion": 1, "title": info.get("_songName", "Untitled"), "artist": info.get("_songAuthorName", ""), + "difficulty": selected["_difficulty"], "difficulties": [item["_difficulty"] for item in refs], "filename": filename, + "bpm": bpm, "offsetSeconds": float(info.get("_songTimeOffset", 0)), "duration": cached["duration"], "waveform": cached["waveform"], + "chart": chart, "chartHash": chart_identity(folder), "audioHash": audio_hash, "sections": section_plan.get("sections", []), "findings": findings, + "mappingPlan": metadata["mappingPlan"], "provenance": {key: metadata["provenance"].get(key) for key in ("seed", "engine", "model", "revision", "mappingPlan")}, + "colors": {"left": scheme.get("saberAColor"), "right": scheme.get("saberBColor")}, + "timingVerified": metadata["analysis"].get("status") == "timing_verified", + } + + +@router.get("/api/jobs/{job_id}/preview") +def job_preview(job_id: str, difficulty: str | None = Query(None)) -> dict[str, Any]: + status, folder = _map(job_id) + result = preview_payload(folder, difficulty) + analysis_path = folder / "_beatforge" / "analysis.json" + has_grid = analysis_path.is_file() and len(read_json(confined(folder, "_beatforge/analysis.json")).get("beatGrid", [])) >= 2 + result.update({"jobId": job_id, "audioUrl": f"/api/jobs/{job_id}/audio", "canRevise": status.get("localStatus") in {"playtest_candidate", "unconfirmed_pack"} and has_grid and not result["chart"].get("bpmEvents") and (folder / "_beatforge" / "sections.json").is_file()}) + return result + + +@router.get("/api/jobs/{job_id}/audio") +def job_audio(job_id: str) -> FileResponse: + _, folder = _map(job_id) + info = read_json(confined(folder, "Info.dat")) + return FileResponse(confined(folder, str(info.get("_songFilename", "song.ogg")))) + + +def interval(item: dict[str, Any], collection: str) -> tuple[float, float]: + start = float(item.get("b", 0)) + end = float(item.get("tb", start)) if collection in {"sliders", "burstSliders"} else start + float(item.get("d", 0)) if collection == "obstacles" else start + if collection.endswith("EventBoxGroups"): + if collection == "vfxEventBoxGroups": + raise ValueError("VFX event box timing cannot yet be revised safely") + offsets = [0.0] + for box in item.get("e", []): + if float(box.get("w", 0)) != 0: + raise ValueError("Distributed lighting timing cannot yet be revised safely") + for key in ("e", "l", "t"): + offsets.extend(float(event.get("b", 0)) for event in box.get(key, [])) + if any(not math.isfinite(offset) or offset < 0 for offset in offsets): + raise ValueError("Invalid relative lighting timing") + end = start + max(offsets) + if not math.isfinite(start) or not math.isfinite(end) or end < start: + raise ValueError("An object has invalid timing") + return start, end + + +def merge_section(original: dict[str, Any], candidate: dict[str, Any], start: float, end: float) -> dict[str, Any]: + """A half-open [start, end) edit. Crossing objects require a wider selection.""" + if not math.isfinite(start) or not math.isfinite(end) or end <= start or start < 0: + raise ValueError("Select an increasing, nonnegative beat range") + result = copy.deepcopy(original) + for collection in COLLECTIONS: + def inside(item: dict[str, Any]) -> bool: + head, tail = interval(item, collection) + overlaps = head < end and tail >= start + included = start <= head < end and tail < end + if tail > head and overlaps and not included: + raise ValueError(f"Selection cuts through {collection} at beat {head:g}. Widen the selection to include the whole object.") + return included + outside = [copy.deepcopy(item) for item in original.get(collection, []) if not inside(item)] + replacement = [copy.deepcopy(item) for item in candidate.get(collection, []) if inside(item)] + if collection in original or replacement: + result[collection] = sorted(outside + replacement, key=lambda item: float(item.get("b", 0))) + return result + + +class RevisionRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + difficulty: str + startBeat: float = Field(ge=0) + endBeat: float = Field(gt=0) + baseHash: str = Field(pattern="^[a-f0-9]{64}$") + seed: int = Field(default=42, ge=0, le=2147483647) + mappingPlan: dict[str, Any] = Field(default_factory=dict) + + +def _run_revision(job_id: str) -> None: + from beatforge import api + if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + from choreography import generate_all + from timing_export import project_map_timing, project_plan_timing + from validate_map import validate_package + from beatforge.mapping_plan import normalize_mapping_plan + start_time = time.time() + status = api._read_status(job_id) + request = status["revision"] + folder = api._job_dir(job_id) / "map" + try: + if api._cancel_requested(job_id): + raise InterruptedError("Revision cancelled") + status.update({"status": "running", "stage": "choreography", "detail": "Composing the selected passage from cached audio analysis"}) + api._write_status(job_id, status) + analysis = read_json(folder / "_beatforge" / "analysis.json") + sections = read_json(folder / "_beatforge" / "sections.json") + plan = normalize_mapping_plan(request["mappingPlan"]) + maps, report = generate_all(analysis, sections, request["seed"], difficulties=[request["difficulty"]], mapping_plan=plan) + if api._cancel_requested(job_id): + raise InterruptedError("Revision cancelled") + info = read_json(folder / "Info.dat") + ref = next(item for item in chart_refs(info) if item["_difficulty"] == request["difficulty"]) + path = confined(folder, ref["_beatmapFilename"]) + original = read_json(path) + candidate = project_map_timing(maps[request["difficulty"]], analysis) + merged = merge_section(original, candidate, request["startBeat"], request["endBeat"]) + path.write_text(json.dumps(merged, indent=2) + "\n", encoding="utf-8") + provenance_path = folder / "_beatforge" / "provenance.json" + provenance = read_json(provenance_path) if provenance_path.is_file() else {} + provenance.update({"revision": {**request, "parentJob": status["revisionOf"], "preservedOutsideSelection": True}, "releaseGate": {}, "humanPlaytests": [], "status": "unreviewed_revision"}) + # A revision never inherits reviews or human evidence for the original artifact. + for key in ("reviews", "providerReviews", "playtests", "vrPlaytestPassed", "freshSightReadPassed"): + provenance.pop(key, None) + provenance_path.write_text(json.dumps(provenance, indent=2), encoding="utf-8") + plan_path = folder / "_beatforge" / "mapping_plan.json" + parent_plan = read_json(plan_path) if plan_path.is_file() else {} + updated_plan = project_plan_timing(report.get("mappingPlan", {"controls": plan, "sections": sections.get("sections", [])}), analysis) + updated_plan["appliesTo"] = {"difficulty": request["difficulty"], "startBeat": request["startBeat"], "endBeat": request["endBeat"]} + updated_plan["parentPlan"] = parent_plan + plan_path.write_text(json.dumps(updated_plan, indent=2), encoding="utf-8") + (folder / "_beatforge" / "revision_choreography.json").write_text(json.dumps(report, indent=2), encoding="utf-8") + status.update({"status": "validating", "stage": "validating", "detail": "Checking the complete chart, including both edit boundaries"}) + api._write_status(job_id, status) + qa = validate_package(folder) + qa_payload = qa.to_dict() + (folder / "_beatforge" / "qa_report.json").write_text(json.dumps(qa_payload, indent=2), encoding="utf-8") + if api._cancel_requested(job_id): + raise InterruptedError("Revision cancelled") + if qa.errors: + status.update({"status": "invalid", "localStatus": "invalid", "detail": "The revision failed validation. The original map is unchanged; widen the range or adjust the plan."}) + else: + package_map(folder, api._job_dir(job_id) / "map.zip") + status.update({"status": "review_required", "localStatus": "playtest_candidate" if analysis.get("status") == "timing_verified" else "unconfirmed_pack", "detail": "Section revised and validated. Review the preview before installing.", "timingVerified": analysis.get("status") == "timing_verified"}) + status.update({"stage": "ready", "summary": summarize_map(folder), "qa": {"status": qa.status, "errors": len(qa.errors), "warnings": len(qa.warnings)}}) + except InterruptedError: + status.update({"status": "cancelled", "detail": "Revision cancelled. Original map preserved."}) + except Exception as error: + status.update({"status": "error", "error": str(error), "detail": f"Revision could not complete: {error}. Original map preserved."}) + status["elapsed"] = round(time.time() - start_time, 2) + api._write_status(job_id, status) + + +@router.post("/api/jobs/{job_id}/revise") +def revise_section(job_id: str, request: RevisionRequest, background_tasks: BackgroundTasks) -> dict[str, Any]: + from beatforge import api + from beatforge.mapping_plan import normalize_mapping_plan + status, source = _map(job_id) + if status.get("localStatus") not in {"playtest_candidate", "unconfirmed_pack"}: + raise HTTPException(409, "Start a revision from a completed, structurally valid map") + if api._qa_report(source)["errors"]: + raise HTTPException(409, "The source map no longer passes QA") + if any(path.is_symlink() or getattr(path, 'is_junction', lambda: False)() for path in source.rglob('*')): + raise HTTPException(422, "Revision source must contain regular files, not linked files") + if request.endBeat <= request.startBeat: + raise HTTPException(422, "The end beat must follow the start beat") + if chart_identity(source) != request.baseHash: + raise HTTPException(409, "This map changed. Reload the preview before revising it.") + try: + plan = normalize_mapping_plan(request.mappingPlan) + except ValueError as error: + raise HTTPException(422, str(error)) from error + info = read_json(source / "Info.dat") + ref = next((item for item in chart_refs(info) if item.get("_difficulty") == request.difficulty), None) + if not ref: + raise HTTPException(422, "Select a difficulty in this map") + original = read_json(confined(source, ref["_beatmapFilename"])) + if original.get("bpmEvents"): + raise HTTPException(422, "Section revisions require a constant exported BPM clock. Charts with BPM events are preserved for preview only.") + try: + merge_section(original, original, request.startBeat, request.endBeat) + except ValueError as error: + raise HTTPException(422, str(error)) from error + for required in ("analysis.json", "sections.json"): + if not (source / "_beatforge" / required).is_file(): + raise HTTPException(409, "This imported map has no cached analysis for regeneration") + analysis = read_json(source / "_beatforge" / "analysis.json") + if len(analysis.get("beatGrid", [])) < 2: + raise HTTPException(409, "This map has no adopted beat/sample grid for regeneration") + if request.endBeat * 60 / float(info["_beatsPerMinute"]) > float(analysis.get("durationSeconds", 0)) + 1e-6: + raise HTTPException(422, "The selected range extends beyond the song") + new_id = uuid.uuid4().hex[:12] + destination = api._job_dir(new_id) + destination.mkdir(parents=True) + shutil.copytree(source, destination / "map") + for path in api._job_dir(job_id).glob("input.*"): + shutil.copy2(path, destination / path.name) + revision = request.model_dump() + revision["mappingPlan"] = plan + new_status = {"id": new_id, "status": "queued", "title": status.get("title"), "artist": status.get("artist"), "mapper": status.get("mapper", "BeatForge"), "seed": request.seed, "difficulties": status.get("difficulties", []), "startedAt": time.time(), "stages": [], "revisionOf": job_id, "revision": revision, "mappingPlan": plan, "installed": False, "releaseGate": {}} + api._write_status(new_id, new_status) + background_tasks.add_task(_run_revision, new_id) + return {"id": new_id, "status": "queued", "revisionOf": job_id} diff --git a/tests/connector_upload_retry.test.cjs b/tests/connector_upload_retry.test.cjs new file mode 100644 index 0000000..4eb007b --- /dev/null +++ b/tests/connector_upload_retry.test.cjs @@ -0,0 +1,59 @@ +// Unit-level adapter tests: no browser, network, or real credentials. +const {test} = require('node:test'); +const assert = require('node:assert/strict'); +const vm = require('node:vm'); +const fs = require('node:fs'); +const path = require('node:path'); + +for (const scenario of ['missing', 'expired', 'ready', 'unauthorized', 'writing']) { + test(`cached upload: ${scenario}`, async () => { + const elements = new Map(); + const element = id => { + if (!elements.has(id)) elements.set(id, {value:'', checked:false, handlers:{}, + addEventListener(name, fn) {this.handlers[name] = fn;}, + before(){}, setAttribute(){}, replaceChildren(){}}); + return elements.get(id); + }; + let reservations = 0, puts = 0, phase = 0; + const response = (body, status=200) => ({ok:status===200,status,json:async()=>body}); + const fakeFetch = async (url, options) => { + const route = new URL(url).pathname; + if (route === '/api/capabilities') return response({generationMessage:'Test worker'}); + if (route === '/api/jobs') return response(options.method === 'POST' ? {id:'job',state:'failed'} : {jobs:[]}); + if (route === '/api/jobs/job') return response({id:'job',state:'failed'}); + if (route === '/api/uploads') { + reservations++; + return response({id:String(reservations),state:'pending',expires:Date.now()/1000+3600,uploadPath:'/api/upload-bytes'}); + } + if (route === '/api/upload-bytes') { + puts++; + return response({detail:'Stop before job submission'},408); + } + assert.equal(phase, 1); + if (scenario === 'missing') return response({detail:'Missing'},404); + if (scenario === 'unauthorized') return response({detail:'Rejected'},401); + return response({state:scenario === 'expired'?'pending':scenario,expires:0}); + }; + class AudioFile {size=4; name='synthetic.wav';} + const context = {URL, Headers, Event, File:AudioFile, crypto:require('node:crypto').webcrypto, + fetch:fakeFetch, document:{getElementById:element,createElement:()=>element('panel')}, + window:{dispatchEvent(){},BeatForgeApp:{showRemoteJob(){}}},setTimeout(){}}; + vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../web/connector.js'),'utf8'),context); + element('gatewayOrigin').value='http://localhost:8013'; + element('gatewayToken').value='a'.repeat(32); + await element('gatewayConnect').handlers.submit({preventDefault(){}}); + const file = new AudioFile(); + const fields = {audio:file,engine:'premium',difficulties:'Hard',mappingPlan:'{}'}; + const form = {get:key=>fields[key]}; + await assert.rejects(context.window.BeatForgeConnection.generate(form)); + assert.equal(reservations,1); + phase=1; + if (scenario === 'ready') { + // Completed uploads survive reservation expiry and must not be uploaded again. + await context.window.BeatForgeConnection.generate(form); + } else await assert.rejects(context.window.BeatForgeConnection.generate(form)); + const renewed = ['missing','expired'].includes(scenario); + assert.equal(reservations,renewed?2:1); + assert.equal(puts,renewed?2:1); + }); +} diff --git a/tests/test_api.py b/tests/test_api.py index e416e30..f24c6ab 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -15,6 +15,19 @@ from test_pipeline import write_click_track +@pytest.fixture(autouse=True) +def isolate_studio_files(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Endpoint tests must never touch the user's real jobs or game library.""" + from beatforge import api + + levels = tmp_path / "test-custom-levels" + levels.mkdir() + monkeypatch.setenv("BEATSABER_CUSTOM_LEVELS", str(levels)) + monkeypatch.setattr(api, "JOBS_DIR", tmp_path / "test-jobs") + monkeypatch.setattr(api, "IMPORTS_DIR", tmp_path / "test-imports") + monkeypatch.setattr(api, "run_premium_pipeline", lambda **kwargs: {"status": "needs_anchors", "payload": {"message": "test fixture"}}) + + @pytest.fixture(scope="module") def client() -> TestClient: return TestClient(app) @@ -376,7 +389,7 @@ def unexpected_install_error(*args: object, **kwargs: object) -> Path: assert len(dl.content) > 1000 -def test_studio_states_are_the_ten_consumed_release_states() -> None: +def test_studio_states_include_recoverable_terminal_states() -> None: from beatforge.api import STUDIO_STATES, TERMINAL_STATES assert STUDIO_STATES == ( @@ -390,9 +403,11 @@ def test_studio_states_are_the_ten_consumed_release_states() -> None: "release_candidate", "invalid", "error", + "interrupted", + "cancelled", ) assert "validating" not in TERMINAL_STATES - assert len(STUDIO_STATES) == 10 + assert len(STUDIO_STATES) == 12 def test_click_track_serves_analysis_wav_only( @@ -453,6 +468,9 @@ def test_unconfirmed_pack_auto_installs_into_custom_levels( map_dir.mkdir(parents=True) (job_dir / "input.wav").write_bytes(b"RIFF") (map_dir / "Info.dat").write_text('{"_songName":"Draft"}', encoding="utf-8") + reports = map_dir / "_beatforge" + reports.mkdir() + (reports / "qa_report.json").write_text('{"status":"playtest_candidate","errors":[],"warnings":[]}', encoding="utf-8") api._write_status( job_id, { @@ -468,7 +486,7 @@ def test_unconfirmed_pack_auto_installs_into_custom_levels( ) def fake_pipeline(**kwargs: object) -> dict: - return {"status": "invalid", "payload": {"message": "unconfirmed"}} + return {"status": "playtest_candidate", "payload": {"message": "unconfirmed"}} monkeypatch.setattr(api, "run_premium_pipeline", fake_pipeline) monkeypatch.setattr(api, "summarize_map", lambda _path: {"difficulties": {}}) @@ -493,6 +511,9 @@ def test_unconfirmed_pack_can_be_downloaded( job_dir = tmp_path / job_id job_dir.mkdir() (job_dir / "map.zip").write_bytes(b"PK" + b"\x00" * 1200) + reports = job_dir / "map" / "_beatforge" + reports.mkdir(parents=True) + (reports / "qa_report.json").write_text('{"status":"playtest_candidate","errors":[],"warnings":[]}', encoding="utf-8") api._write_status( job_id, {"id": job_id, "status": "invalid", "localStatus": "unconfirmed_pack", "title": "Draft"}, @@ -749,7 +770,7 @@ def __init__(self, code: int) -> None: self.stdout = io.StringIO(json.dumps({"status": "x", "message": "pipeline"})) self.stderr = io.StringIO("BEATFORGE_PROGRESS\tanalysis\tdecoding audio\n") - def wait(self) -> int: + def wait(self, timeout: float | None = None) -> int: return self.returncode def run_with(code: int) -> str: @@ -789,7 +810,7 @@ def __init__(self, *args: object, **kwargs: object) -> None: "BEATFORGE_PROGRESS\tchoreography\tsolving joint CP-SAT for ExpertPlus\n" ) - def wait(self) -> int: + def wait(self, timeout: float | None = None) -> int: return self.returncode seen: list[tuple[str, str]] = [] diff --git a/tests/test_browser.py b/tests/test_browser.py index b17f6d8..1cf04bb 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -4,6 +4,7 @@ import threading import time +import socket from pathlib import Path import pytest @@ -12,7 +13,6 @@ ROOT = Path(__file__).resolve().parents[1] -STUDIO_PORT = 18765 def _playwright_chromium(): @@ -28,12 +28,22 @@ def _playwright_chromium(): @pytest.fixture(scope="module") -def studio_url(): +def studio_url(tmp_path_factory): import uvicorn + from beatforge import api, learning - config = uvicorn.Config(app, host="127.0.0.1", port=STUDIO_PORT, log_level="warning") + isolated = tmp_path_factory.mktemp("browser-studio") + patches = pytest.MonkeyPatch() + for field in ("JOBS_DIR", "IMPORTS_DIR", "METADATA_DIR"): + patches.setattr(api, field, isolated / field.lower()) + patches.setattr(learning, "LEARNING_FILE", isolated / "learning.json") + patches.setattr(api, "find_custom_levels", lambda: None) + sock = socket.socket() + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning") server = uvicorn.Server(config) - thread = threading.Thread(target=server.run, daemon=True) + thread = threading.Thread(target=server.run, kwargs={"sockets": [sock]}, daemon=True) thread.start() deadline = time.time() + 8 while time.time() < deadline: @@ -42,12 +52,14 @@ def studio_url(): time.sleep(0.05) if not server.started: pytest.skip("could not bind the headless studio server") - yield f"http://127.0.0.1:{STUDIO_PORT}" + yield f"http://127.0.0.1:{port}" server.should_exit = True thread.join(timeout=4) + sock.close() + patches.undo() -@pytest.fixture(scope="module") +@pytest.fixture def page(studio_url: str): playwright, browser = _playwright_chromium() context = browser.new_context(viewport={"width": 1280, "height": 900}) @@ -112,8 +124,11 @@ def test_playwright_mobile_tablet_and_200_percent_zoom(studio_url: str) -> None: for options in cases: playwright, browser, context, opened = _open_studio(studio_url, **options) try: - assert opened.locator("article.connector").count() == 3 + assert opened.locator("#previewPanel").is_visible() + assert opened.locator("#historyPanel").is_visible() + assert opened.locator("#learningPanel").is_visible() assert opened.locator("#generate").is_visible() + assert opened.evaluate("document.documentElement.scrollWidth <= window.innerWidth + 1") assert opened.locator('img[alt="BeatForge"]').count() == 1 opened.locator("#generate").click() assert opened.locator("#statusTitle").inner_text() == "Select a mastered audio file first." @@ -128,7 +143,7 @@ def test_playwright_keyboard_reaches_generate_and_refuses_without_audio(studio_u try: opened.locator("body").click(position={"x": 8, "y": 8}) seen: list[str] = [] - for _ in range(24): + for _ in range(45): opened.keyboard.press("Tab") focused = opened.evaluate("document.activeElement && document.activeElement.id") if focused: @@ -171,6 +186,7 @@ def test_playwright_offline_and_unconfigured_codex_state(studio_url: str) -> Non assert page.locator("#gameStatus").inner_text() == "Beat Saber check unavailable" page.evaluate("() => { document.getElementById('timingAi').hidden = false; }") page.locator('.timing-ai-btn[data-provider="codex"]').click() + page.locator("#providerDialog").wait_for(state="visible") assert page.locator("#providerDialog").is_visible() assert "OpenAI Codex setup" in page.locator("#providerName").inner_text() assert "never returns it to this page" in page.locator("#providerDialog").inner_text() @@ -199,6 +215,7 @@ def test_playwright_webmcp_mock_registers_and_runs_collaboration_loop(studio_url try: opened.goto(studio_url, wait_until="networkidle") opened.wait_for_function("() => document.getElementById('webmcpStatus').textContent === 'WebMCP ready'") + opened.locator('details.connection-details > summary').click() result = opened.evaluate( """ async () => { @@ -208,13 +225,16 @@ def test_playwright_webmcp_mock_registers_and_runs_collaboration_loop(studio_url await call('set_mapping_plan')({ title: 'Agent Shaped Rain', creativeBrief: 'Readable Expert chorus with a bright syncopated lift.', - difficulties: ['Normal', 'Hard', 'Expert'] + difficulties: ['Normal', 'Hard', 'Expert'], + mappingPlan: {density: 0.75, noBombs: true, style: 'flow'} }); const generated = JSON.parse(await call('generate_beatmap')({ reason: 'Turn the shared brief into a preview.' })); const evidence = JSON.parse(await call('record_human_playtest')({ difficulty: 'Expert', speed: 'full', passed: true, tester: 'Human tester', notes: 'Real person supplied this evidence.' })); const context = JSON.parse(await call('get_studio_context')({ includeActivity: true })); + const preview = JSON.parse(await call('get_chart_preview')({})); + const learning = JSON.parse(await call('get_learning_summary')({})); return { names: tools.map(tool => tool.name).sort(), title: document.getElementById('title').value, @@ -222,6 +242,9 @@ def test_playwright_webmcp_mock_registers_and_runs_collaboration_loop(studio_url generated, evidence, context, + preview, + learning, + planSchema: tools.find(tool => tool.name === 'set_mapping_plan').inputSchema.properties.mappingPlan, activity: document.getElementById('agentActivity').innerText }; } @@ -230,16 +253,27 @@ def test_playwright_webmcp_mock_registers_and_runs_collaboration_loop(studio_url assert result["names"] == sorted( [ "get_studio_context", + "find_song_metadata", + "import_song_preview", "set_mapping_plan", "load_collaboration_demo", "generate_beatmap", "review_current_beatmap", "record_human_playtest", + "get_chart_preview", + "get_learning_summary", + "record_mapping_feedback", ] ) assert result["title"] == "Agent Shaped Rain" assert "Readable Expert chorus" in result["brief"] assert result["generated"]["mode"] == "rights-safe-demo" + assert result["context"]["mappingPlan"]["mappingPlan"]["density"] == 0.75 + assert result["context"]["mappingPlan"]["mappingPlan"]["noBombs"] is True + assert result["preview"]["notes"] > 0 + assert result["learning"]["feedbackCount"] == 0 + assert result["planSchema"]["additionalProperties"] is False + assert result["planSchema"]["properties"]["density"]["minimum"] == 0.5 assert result["evidence"]["releaseGate"]["evidenceCount"] == 1 assert result["context"]["job"]["demo"] is True assert "Agent called generate_beatmap" in result["activity"] diff --git a/tests/test_connector.py b/tests/test_connector.py new file mode 100644 index 0000000..4c72620 --- /dev/null +++ b/tests/test_connector.py @@ -0,0 +1,38 @@ +import json +from fastapi.testclient import TestClient +from beatforge.connector import create_app, Principal +from beatforge.connector_store import ConnectorStore + + +def test_mcp_handshake_tools_and_shared_editorial_rules(tmp_path): + token = 'a' * 32 + app = create_app(database=tmp_path / 'jobs.db', credentials={token: Principal('alice', frozenset({'read'}))}, hosts=['testserver']) + headers = {'Authorization': f'Bearer {token}', 'Accept': 'application/json, text/event-stream'} + with TestClient(app) as client: + assert client.post('/mcp', json={}).status_code == 401 + schema = client.get('/openapi.json', headers=headers).json() + assert schema['security'] == [{'BearerAuth': []}] + assert schema['paths']['/health']['get']['security'] == [] + assert '/api/feedback' in schema['paths'] + reply = client.post('/mcp', headers=headers, json={'jsonrpc': '2.0', 'id': 1, 'method': 'initialize', 'params': {'protocolVersion': '2025-06-18', 'capabilities': {}, 'clientInfo': {'name': 'test', 'version': '1'}}}) + assert reply.status_code == 200 + assert reply.json()['result']['serverInfo']['name'] == 'BeatForge' + tools = client.post('/mcp', headers=headers, json={'jsonrpc': '2.0', 'id': 2, 'method': 'tools/list', 'params': {}}).json()['result']['tools'] + assert 'resolve_mapping_plan' in [tool['name'] for tool in tools] + plan = {'brief': 'lighter verses, no bombs', 'style': 'flow'} + rest = client.post('/api/mapping-plan/resolve', headers=headers, json=plan) + result = client.post('/mcp', headers=headers, json={'jsonrpc': '2.0', 'id': 3, 'method': 'tools/call', 'params': {'name': 'resolve_mapping_plan', 'arguments': {'plan': plan}}}).json()['result'] + assert not result.get('isError'), result + assert json.loads(result['content'][0]['text']) == rest.json() + assert rest.json()['noBombs'] is True + assert rest.json()['verseDensity'] < 1 + + +def test_rest_ownership_and_scope_enforcement(tmp_path): + path = tmp_path / 'jobs.db' + app = create_app(database=path, credentials={'a'*32: Principal('alice', frozenset({'read'})), 'b'*32: Principal('bob', frozenset({'read', 'edit'}))}, hosts=['testserver']) + job = ConnectorStore(path).submit('alice', 'one', {}) + with TestClient(app) as client: + assert client.get('/api/jobs/'+job['id'], headers={'Authorization': 'Bearer '+'b'*32}).status_code == 404 + assert client.post('/api/jobs/'+job['id']+'/cancel', headers={'Authorization': 'Bearer '+'a'*32}).status_code == 403 + assert client.get('/api/capabilities', headers={'Authorization': 'Bearer '+'a'*32}).json()['generationAvailable'] is False diff --git a/tests/test_connector_artifacts.py b/tests/test_connector_artifacts.py new file mode 100644 index 0000000..242b0c7 --- /dev/null +++ b/tests/test_connector_artifacts.py @@ -0,0 +1,128 @@ +import json +import zipfile +from pathlib import Path + +from beatforge.connector_export import prepare_artifacts +from beatforge.connector_worker import run_once +from test_connector_worker import setup_job + +ROOT = Path(__file__).resolve().parents[1] + + +def test_stalled_artifact_releases_quota_and_can_retry(tmp_path, monkeypatch): + import asyncio + import pytest + from fastapi import HTTPException, Request + from beatforge.connector_artifacts import Artifacts + from beatforge.connector_store import ConnectorStore + store = ConnectorStore(tmp_path/'jobs.db') + artifacts = Artifacts(store, tmp_path/'artifacts') + store.submit('alice', 'test', {}) + job = store.claim('alice') + monkeypatch.setattr('beatforge.connector_artifacts.ARTIFACT_TIMEOUT_SECONDS', 0.02) + async def exercise(): + delivered = False + async def stalled(): + nonlocal delivered + if not delivered: + delivered = True + return {'type':'http.request','body':b'ab','more_body':True} + await asyncio.sleep(10) + scope = {'type':'http','headers':[(b'content-length',b'4')]} + with pytest.raises(HTTPException) as failure: + await artifacts.receive('alice',job['id'],job['leaseToken'],'audio.ogg',Request(scope,stalled)) + assert failure.value.status_code == 408 + assert not list(artifacts.root.iterdir()) + with store.connect() as db: + assert db.execute('SELECT COUNT(*) FROM artifacts').fetchone()[0] == 0 + async def retry(): return {'type':'http.request','body':b'data','more_body':False} + await artifacts.receive('alice',job['id'],job['leaseToken'],'audio.ogg',Request(scope,retry)) + assert len(artifacts.manifest('alice',job['id'],job['leaseToken'])) == 1 + asyncio.run(exercise()) + + +def sample_map(destination): + # Repository-owned synthetic fixture, not an uploaded archive. + with zipfile.ZipFile(ROOT/'web/assets/demo/map.zip') as archive: + archive.extractall(destination) + + +def test_export_excludes_private_pipeline_files(tmp_path): + folder = tmp_path/'map' + sample_map(folder) + (folder/'private-corpus.txt').write_text('private corpus') + outputs = prepare_artifacts(folder, tmp_path/'publish', {'brief': 'test'}) + with zipfile.ZipFile(outputs['map.zip']) as archive: + assert 'Info.dat' in archive.namelist() + assert all(not name.startswith('_beatforge/') for name in archive.namelist()) + assert 'private-corpus.txt' not in archive.namelist() + preview = json.loads(outputs['preview-Hard.json'].read_text()) + assert preview['chart']['colorNotes'] + assert preview['mappingPlan'] == {'brief': 'test'} + assert 'provenance' not in preview + assert json.loads(outputs['qa.json'].read_text())['humanPlaytestRequired'] is True + + +def test_worker_publishes_real_fixture_artifacts_and_downloads(tmp_path, monkeypatch): + client, job, _ = setup_job(tmp_path) + def pipeline(**kwargs): + sample_map(kwargs['output']) + return {'status': 'playtest_candidate'} + monkeypatch.setattr('beatforge.connector_worker.run_premium_pipeline', pipeline) + with client: + client.headers['Authorization'] = 'Bearer '+'w'*32 + assert run_once(client, tmp_path/'worker') + client.headers['Authorization'] = 'Bearer '+'a'*32 + result = client.get('/api/jobs/'+job['id']).json() + assert result['state'] == 'completed' + assert result['result']['humanPlaytestRequired'] is True + artifacts = client.get(f"/api/jobs/{job['id']}/artifacts").json()['artifacts'] + assert len(artifacts) == 8 + preview = next(item for item in artifacts if item['name'] == 'preview-Hard.json') + response = client.get(preview['downloadPath']) + assert response.status_code == 200 + assert response.json()['difficulty'] == 'Hard' + assert response.headers['cache-control'] == 'no-store' + assert client.get(preview['downloadPath'], headers={'Authorization': 'Bearer '+'b'*32}).status_code == 404 + assert client.get(preview['downloadPath'], headers={'Authorization': ''}).status_code == 401 + assert client.get(preview['downloadUrl'], headers={'Authorization': ''}).status_code == 200 + assert client.get(preview['downloadUrl']+'broken', headers={'Authorization': ''}).status_code == 401 + wrong_artifact = preview['downloadUrl'].replace(preview['id'], 'f'*32, 1) + assert client.get(wrong_artifact, headers={'Authorization': ''}).status_code == 401 + import datetime + import jwt.api_jwt + class FutureClock(datetime.datetime): + @classmethod + def now(cls, tz=None): + return datetime.datetime.now(tz) + datetime.timedelta(minutes=6) + monkeypatch.setattr(jwt.api_jwt, 'datetime', FutureClock) + assert client.get(preview['downloadUrl'], headers={'Authorization': ''}).status_code == 401 + + +def test_cancelled_artifacts_cannot_publish_and_missing_outputs_block_finish(tmp_path): + client, job, _ = setup_job(tmp_path) + worker = {'Authorization': 'Bearer '+'w'*32} + with client: + lease = client.post('/api/worker/claim', headers=worker).json()['job']['leaseToken'] + worker['X-BeatForge-Lease'] = lease + finish = f"/api/worker/jobs/{job['id']}/finish" + assert client.post(finish, headers=worker, json={'status': 'playtest_candidate'}).status_code == 422 + endpoint = f"/api/worker/jobs/{job['id']}/artifacts/qa.json" + assert client.put(endpoint, content=b'{}', headers=worker).status_code == 200 + assert client.put(endpoint, content=b'{}', headers=worker).status_code == 422 + assert client.get(f"/api/jobs/{job['id']}/artifacts").json()['artifacts'] == [] + assert client.post(f"/api/jobs/{job['id']}/cancel").status_code == 200 + assert client.put(endpoint, content=b'{}', headers=worker).status_code == 422 + assert client.post(finish, headers=worker, json={'status': 'needs_anchors'}).status_code == 422 + assert client.get(f"/api/jobs/{job['id']}/artifacts").json()['artifacts'] == [] + + +def test_http_access_logs_redact_download_tickets(): + import logging + import httpx + from beatforge.connector_logging import DownloadTicketFilter + record = logging.LogRecord('httpx', logging.INFO, __file__, 1, 'HTTP Request: %s %s', + ('GET', httpx.URL('https://example.com/downloads/123?ticket=private')), None) + assert DownloadTicketFilter().filter(record) + assert 'private' not in record.getMessage() + assert '[redacted]' in record.getMessage() diff --git a/tests/test_connector_auth.py b/tests/test_connector_auth.py new file mode 100644 index 0000000..98229da --- /dev/null +++ b/tests/test_connector_auth.py @@ -0,0 +1,85 @@ +import time +from types import SimpleNamespace + +import jwt +import pytest +from cryptography.hazmat.primitives.asymmetric import rsa +from fastapi.testclient import TestClient + +from beatforge.connector import create_app +from beatforge.connector_auth import OAuthSettings, OAuthVerifier +from beatforge.connector_store import ConnectorStore + + +@pytest.fixture +def signing(monkeypatch): + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + monkeypatch.setattr(jwt.PyJWKClient, 'get_signing_key_from_jwt', lambda self, token: SimpleNamespace(key=key.public_key())) + settings = OAuthSettings('https://identity.example/', 'https://identity.example/jwks', 'https://forge.example/mcp') + + def issue(**changes): + claims = {'iss': settings.issuer, 'aud': settings.resource, 'sub': 'alice', + 'iat': int(time.time()), 'exp': int(time.time()) + 300, 'scope': 'read edit worker'} + claims.update(changes) + return jwt.encode(claims, key, algorithm='RS256') + return settings, issue + + +def test_oauth_discovery_cors_and_identity(tmp_path, signing): + settings, issue = signing + app = create_app(database=tmp_path/'jobs.db', oauth=settings, hosts=['testserver'], origins=['https://studio.example']) + actor = OAuthVerifier(settings).verify(issue()) + assert actor.scopes == frozenset({'read', 'edit'}) + job = ConnectorStore(tmp_path/'jobs.db').submit(actor.owner, 'one', {}) + with TestClient(app) as client: + metadata = client.get('/.well-known/oauth-protected-resource/mcp') + assert metadata.status_code == 200 + assert metadata.json()['resource'] == settings.resource + denied = client.get('/api/capabilities', headers={'Origin': 'https://studio.example'}) + assert denied.status_code == 401 + assert settings.metadata_url in denied.headers['www-authenticate'] + assert denied.headers['access-control-allow-origin'] == 'https://studio.example' + preflight = client.options('/mcp', headers={'Origin': 'https://studio.example', 'Access-Control-Request-Method': 'POST', 'Access-Control-Request-Headers': 'authorization,mcp-protocol-version'}) + assert preflight.status_code == 200 + assert client.get('/api/jobs/'+job['id'], headers={'Authorization': 'bearer '+issue()}).status_code == 200 + assert client.get('/api/jobs/'+job['id'], headers={'Authorization': 'Bearer '+issue(sub='bob')}).status_code == 404 + assert client.get('/health', headers={'host': 'attacker.example'}).status_code == 400 + + +@pytest.mark.parametrize('changes', [ + {'aud': 'https://different.example/mcp'}, {'iss': 'https://untrusted.example/'}, + {'exp': 1}, {'sub': ''}, {'scope': ['read']}, {'iat': int(time.time()) + 3600}, +]) +def test_invalid_access_tokens_rejected(tmp_path, signing, changes): + settings, issue = signing + app = create_app(database=tmp_path/'jobs.db', oauth=settings, hosts=['testserver']) + with TestClient(app) as client: + response = client.get('/api/capabilities', headers={'Authorization': 'Bearer '+issue(**changes)}) + assert response.status_code == 401 + assert response.json() == {'detail': 'Authentication required'} + + +def test_revocation_missing_claims_and_algorithm(signing): + settings, issue = signing + revoked = OAuthSettings(settings.issuer, settings.jwks_url, settings.resource, frozenset({'alice'})) + with pytest.raises(jwt.InvalidTokenError): + OAuthVerifier(revoked).verify(issue()) + with pytest.raises(jwt.InvalidTokenError): + OAuthVerifier(settings).verify(jwt.encode({'sub': 'alice'}, 'secret', algorithm='HS256')) + with pytest.raises(jwt.InvalidTokenError): + OAuthVerifier(settings).verify(issue(exp=None)) + + +def test_jwks_outage_fails_closed(tmp_path, signing, monkeypatch): + settings, issue = signing + def unavailable(self, token): + raise jwt.PyJWKClientConnectionError('offline') + monkeypatch.setattr(jwt.PyJWKClient, 'get_signing_key_from_jwt', unavailable) + with TestClient(create_app(database=tmp_path/'jobs.db', oauth=settings, hosts=['testserver'])) as client: + assert client.get('/api/capabilities', headers={'Authorization': 'Bearer '+issue()}).status_code == 401 + + +@pytest.mark.parametrize('url', ['http://id.example/', 'https://id.example/?x=1', 'https://user:secret@id.example/', 'https://id.example/\n']) +def test_insecure_metadata_configuration_rejected(url): + with pytest.raises(ValueError): + OAuthSettings(url, 'https://id.example/jwks', 'https://forge.example/mcp') diff --git a/tests/test_connector_editorial.py b/tests/test_connector_editorial.py new file mode 100644 index 0000000..1706faa --- /dev/null +++ b/tests/test_connector_editorial.py @@ -0,0 +1,104 @@ +import json + +import pytest +from fastapi.testclient import TestClient + +from beatforge.connector import Principal, create_app +from beatforge.connector_worker import run_once +from test_connector_artifacts import sample_map + + +@pytest.fixture +def editor(tmp_path, monkeypatch): + app = create_app(database=tmp_path/'jobs.db', hosts=['testserver'], credentials={ + 'a'*32: Principal('alice', frozenset({'read', 'generate', 'edit', 'feedback'})), + 'b'*32: Principal('bob', frozenset({'read', 'feedback'})), + 'v'*32: Principal('alice', frozenset({'read'})), + 'w'*32: Principal('alice', frozenset({'worker'}))}) + counter = [0] + def pipeline(**kwargs): + counter[0] += 1 + sample_map(kwargs['output']) + if counter[0] > 1: + path = kwargs['output']/'HardStandard.dat' + chart = json.loads(path.read_text()) + chart['colorNotes'][0]['b'] += .125 + path.write_text(json.dumps(chart)) + return {'status': 'playtest_candidate'} + monkeypatch.setattr('beatforge.connector_worker.run_premium_pipeline', pipeline) + with TestClient(app) as client: + client.headers['Authorization'] = 'Bearer '+'a'*32 + upload = client.post('/api/uploads', json={'filename': 'test.wav', 'size': 4}).json() + client.put(upload['uploadPath'], content=b'data').raise_for_status() + jobs = [] + for key in ('a', 'b'): + response = client.post('/api/jobs', json={'uploadId': upload['id'], 'idempotencyKey': key, 'title': key}) + response.raise_for_status() + jobs.append(response.json()['id']) + client.headers['Authorization'] = 'Bearer '+'w'*32 + run_once(client, tmp_path/'worker') + client.headers['Authorization'] = 'Bearer '+'a'*32 + yield client, jobs + + +def test_history_preview_and_qa_share_account_scope(editor): + client, jobs = editor + history = client.get('/api/jobs?limit=1').json() + assert history['total'] == 2 and len(history['jobs']) == 1 + assert client.get('/api/jobs?limit=0').status_code == 422 + preview = client.get(f'/api/jobs/{jobs[0]}/preview?difficulty=Hard') + assert preview.status_code == 200 + assert preview.json()['chart']['colorNotes'] + assert client.get(preview.json()['audioUrl'], headers={'Authorization': ''}).status_code == 200 + assert client.get(f'/api/jobs/{jobs[0]}/qa').json()['humanPlaytestRequired'] is True + bob = {'Authorization': 'Bearer '+'b'*32} + assert client.get('/api/jobs', headers=bob).json()['total'] == 0 + assert client.get(f'/api/jobs/{jobs[0]}/preview', headers=bob).status_code == 404 + assert client.get(f'/api/jobs/{jobs[0]}/qa', headers=bob).status_code == 404 + + +def test_feedback_is_hash_bound_idempotent_and_scope_checked(editor): + client, jobs = editor + payload = {'jobId': jobs[0], 'difficulty': 'Hard', 'tester': 'Synthetic test fixture', + 'notes': 'Test feedback only, not a human playtest', 'idempotencyKey': 'feedback-1'} + response = client.post('/api/feedback', json=payload) + assert response.status_code == 200 + assert response.json()['previewSha256'] + preview = client.get(f'/api/jobs/{jobs[0]}/preview').json() + assert response.json()['chartHash'] == preview['chartHash'] + assert client.post('/api/feedback', json=payload).json()['id'] == response.json()['id'] + assert client.get('/api/learning').json()['feedbackCount'] == 1 + exported = client.get('/api/learning/export').json() + assert len(exported['feedback']) == 1 + assert exported['feedback'][0]['chartHash'] == preview['chartHash'] + assert exported['automaticTraining'] is False + assert client.get('/api/learning/export', headers={'Authorization': 'Bearer '+'b'*32}).json()['feedback'] == [] + assert client.get('/api/learning/export', headers={'Authorization': 'Bearer '+'w'*32}).status_code == 403 + suggestions = client.post('/api/learning/suggestions', json={'tester': payload['tester']}).json() + assert suggestions['evidenceCount'] == 1 + assert suggestions['ready'] is False + assert suggestions['changedFields'] == {} + assert client.post('/api/feedback', json={**payload, 'notes': 'changed'}).status_code == 422 + assert client.post('/api/feedback', json={**payload, 'chartHash': 'forged'}).status_code == 422 + assert client.post('/api/feedback', json=payload, headers={'Authorization': 'Bearer '+'v'*32}).status_code == 403 + assert client.post('/api/feedback', json=payload, headers={'Authorization': 'Bearer '+'b'*32}).status_code == 404 + + +def test_comparison_and_presets_are_persistent_and_private(editor): + client, jobs = editor + payload = {'preferredJob': jobs[0], 'alternateJob': jobs[1], 'difficulty': 'Hard', + 'tester': 'Synthetic test fixture', 'idempotencyKey': 'comparison-1'} + response = client.post('/api/comparisons', json=payload) + assert response.status_code == 200 + assert response.json()['preferredHash'] != response.json()['alternateHash'] + assert client.post('/api/comparisons', json={**payload, 'alternateJob': jobs[0]}).status_code == 422 + preset = {'name': 'Light verses', 'mappingPlan': {'brief': 'lighter verses, no bombs'}} + first = client.post('/api/presets', json=preset).json() + assert first['mappingPlan']['noBombs'] is True + assert client.post('/api/presets', json={**preset, 'name': 'LIGHT VERSES'}).json()['id'] == first['id'] + assert len(client.get('/api/presets').json()['presets']) == 1 + assert client.get('/api/presets', headers={'Authorization': 'Bearer '+'b'*32}).json()['presets'] == [] + assert client.get('/api/learning').json()['automaticTraining'] is False + exported = client.get('/api/learning/export').json() + assert exported['comparisons'][0]['preferredHash'] == response.json()['preferredHash'] + assert exported['presets'][0]['id'] == first['id'] diff --git a/tests/test_connector_limits.py b/tests/test_connector_limits.py new file mode 100644 index 0000000..04a44df --- /dev/null +++ b/tests/test_connector_limits.py @@ -0,0 +1,57 @@ +import asyncio +from concurrent.futures import ThreadPoolExecutor + +import pytest +from fastapi.testclient import TestClient +from beatforge.connector import create_app, Principal +from beatforge.connector_artifacts import Artifacts +from beatforge.connector_store import ConnectorStore +from beatforge.connector_limits import bounded_request, MAX_JSON_BYTES + + +def test_global_reservations_include_other_accounts_and_artifacts(tmp_path): + store = ConnectorStore(tmp_path/'jobs.db') + store.BLOB_BUDGET = 12 + Artifacts(store, tmp_path/'artifacts') + store.reserve_upload('alice', 'one.wav', 4) + with store.connect() as db: + db.execute("INSERT INTO artifacts(id,owner,job,lease,name,size) VALUES('id','bob','job','lease','audio.ogg',4)") + store.reserve_upload('charlie', 'two.wav', 4) + with pytest.raises(ValueError, match='Gateway storage capacity'): + store.reserve_upload('dave', 'three.wav', 1) + + +def test_global_reservations_are_atomic_across_accounts(tmp_path): + store = ConnectorStore(tmp_path/'jobs.db') + store.BLOB_BUDGET = 8 + def reserve(owner): + try: + store.reserve_upload(owner, 'audio.wav', 8) + return True + except ValueError: + return False + with ThreadPoolExecutor(max_workers=2) as pool: + assert sorted(pool.map(reserve, ['alice','bob'])) == [False, True] + + +@pytest.mark.parametrize('route', ['/mcp', '/api/mapping-plan/resolve']) +def test_rest_and_mcp_reject_large_json_before_parsing(tmp_path, route): + app = create_app(database=tmp_path/'jobs.db', hosts=['testserver'], + credentials={'a'*32: Principal('alice', frozenset({'read'}))}) + with TestClient(app) as client: + response = client.post(route, content=b'x'*(MAX_JSON_BYTES+1), + headers={'Authorization':'Bearer '+'a'*32, 'Content-Type':'application/json'}) + assert response.status_code == 413 + + +def test_chunked_body_cannot_bypass_limit(): + async def exercise(): + messages = iter([{'type':'http.request','body':b'a'*MAX_JSON_BYTES,'more_body':True}, + {'type':'http.request','body':b'b','more_body':False}]) + sent = [] + async def receive(): return next(messages) + async def send(message): sent.append(message) + async def forbidden(*args): pytest.fail('Oversized payload reached parser') + await bounded_request(forbidden, {'type':'http','method':'POST','headers':[]}, receive, send) + assert sent[0]['status'] == 413 + asyncio.run(exercise()) diff --git a/tests/test_connector_maintenance.py b/tests/test_connector_maintenance.py new file mode 100644 index 0000000..442cb16 --- /dev/null +++ b/tests/test_connector_maintenance.py @@ -0,0 +1,71 @@ +import pytest +import json + +from beatforge.connector_maintenance import recover_uploads +from beatforge.connector_store import ConnectorStore +from beatforge.connector_maintenance import reclaim_artifacts +from beatforge.connector_artifacts import Artifacts + + +def test_obsolete_attempt_cleanup_preserves_results_and_current_leases(tmp_path): + store = ConnectorStore(tmp_path/'jobs.db') + artifacts = Artifacts(store,tmp_path/'artifacts') + store.submit('alice','running',{}) + running = store.claim('alice') + finished = store.submit('bob','finished',{}) + identifiers = ['1'*32,'2'*32,'3'*32] + with store.connect() as db: + db.execute("UPDATE jobs SET state='completed',result=? WHERE id=?", + (json.dumps({'artifacts':[{'id':identifiers[0]}]}),finished['id'])) + for identifier, job, lease in [(identifiers[0],finished['id'],'old'), + (identifiers[1],running['id'],running['leaseToken']), + (identifiers[2],running['id'],'abandoned')]: + db.execute('INSERT INTO artifacts(id,owner,job,lease,name,size) VALUES(?,?,?,?,?,?)', + (identifier,'alice',job,lease,'audio.ogg',4)) + artifacts.path(identifier).write_bytes(b'keep') + assert reclaim_artifacts(store.path)['obsoleteArtifacts'] == 1 + assert all(artifacts.path(i).exists() for i in identifiers) + assert reclaim_artifacts(store.path,apply=True)['reservedBytes'] == 4 + assert all(artifacts.path(i).read_bytes() == b'keep' for i in identifiers[:2]) + assert not artifacts.path(identifiers[2]).exists() + assert reclaim_artifacts(store.path,apply=True)['obsoleteArtifacts'] == 0 + + +def test_offline_recovery_preserves_ready_audio_and_allows_retry(tmp_path): + store = ConnectorStore(tmp_path/'jobs.db') + root = tmp_path/'uploads' + root.mkdir() + interrupted = store.reserve_upload('alice','interrupted.wav',4) + completed = store.reserve_upload('bob','completed.wav',4) + for owner, upload in [('alice',interrupted),('bob',completed)]: + store.begin_upload(owner,upload['id']) + store.complete_upload('bob',completed['id'],'a'*64) + partial = root/(interrupted['id']+'.part') + partial.write_bytes(b'pa') + ready = root/(completed['id']+'.audio') + ready.write_bytes(b'keep') + assert recover_uploads(store.path)['partialFiles'] == 1 + assert partial.read_bytes() == b'pa' + assert store.get_upload('alice',interrupted['id'])['state'] == 'writing' + assert recover_uploads(store.path,apply=True)['interruptedUploads'] == 1 + assert not partial.exists() + assert ready.read_bytes() == b'keep' + assert store.get_upload('bob',completed['id'])['state'] == 'ready' + store.begin_upload('alice',interrupted['id']) + store.abort_upload('alice',interrupted['id']) + assert recover_uploads(store.path,apply=True)['interruptedUploads'] == 0 + + +def test_recovery_checks_all_paths_before_removal(tmp_path): + store = ConnectorStore(tmp_path/'jobs.db') + upload = store.reserve_upload('alice','track.wav',4) + store.begin_upload('alice',upload['id']) + root = tmp_path/'uploads' + root.mkdir() + partial = root/(upload['id']+'.part') + partial.write_bytes(b'pa') + (root/(upload['id']+'.audio')).mkdir() + with pytest.raises(ValueError,match='regular file'): + recover_uploads(store.path,apply=True) + assert partial.read_bytes() == b'pa' + assert store.get_upload('alice',upload['id'])['state'] == 'writing' diff --git a/tests/test_connector_revision.py b/tests/test_connector_revision.py new file mode 100644 index 0000000..e3eb4cd --- /dev/null +++ b/tests/test_connector_revision.py @@ -0,0 +1,75 @@ +import json +import shutil +import pytest + +from fastapi.testclient import TestClient +from beatforge.connector import create_app, Principal +from beatforge.connector_worker import run_once +from beatforge.preview import chart_identity, merge_section, read_json +from test_preview import studio, saved_map, validate_package, choreography + + +@pytest.mark.parametrize('invalid_candidate', [False, True]) +def test_remote_revision_preserves_parent_and_publishes_valid_child(tmp_path, monkeypatch, saved_map, invalid_candidate): + def pipeline(**kwargs): + shutil.copytree(saved_map, kwargs['output']) + return {'status': 'playtest_candidate'} + monkeypatch.setattr('beatforge.connector_worker.run_premium_pipeline', pipeline) + original_generate = choreography.generate_all + def isolated_generate(analysis, sections, seed, **kwargs): + maps, report = original_generate(analysis, sections, seed, corpus_database=tmp_path/'no-corpus.sqlite', **kwargs) + if invalid_candidate: + maps['Easy']['colorNotes'].append({'b':2, 'x':99, 'y':0, 'c':0, 'd':1, 'a':0}) + return maps, report + monkeypatch.setattr(choreography, 'generate_all', isolated_generate) + app = create_app(database=tmp_path/'gateway/jobs.db', hosts=['testserver'], credentials={ + 'a'*32: Principal('alice', frozenset({'read', 'generate', 'edit'})), + 'b'*32: Principal('bob', frozenset({'read', 'edit'})), + 'w'*32: Principal('alice', frozenset({'worker'}))}) + worker_root = tmp_path/'worker' + user = {'Authorization': 'Bearer '+'a'*32} + with TestClient(app, headers=user) as client: + upload = client.post('/api/uploads', json={'filename':'test.wav', 'size':4}).json() + client.put(upload['uploadPath'], content=b'data').raise_for_status() + parent = client.post('/api/jobs', json={'uploadId':upload['id'], 'idempotencyKey':'parent', + 'title':'Revision fixture', 'difficulties':['Easy','Hard']}).json() + client.headers['Authorization'] = 'Bearer '+'w'*32 + run_once(client, worker_root) + client.headers.update(user) + preview = client.get(f"/api/jobs/{parent['id']}/preview?difficulty=Easy").json() + assert preview['canRevise'] is True + request = {'idempotencyKey':'edit', 'difficulty':'Easy', 'startBeat':0, 'endBeat':8, + 'baseHash':preview['chartHash'], 'seed':123, + 'mappingPlan':{'noBombs':True, 'noWalls':True, 'candidateCount':1}} + path = f"/api/jobs/{parent['id']}/revise" + assert client.post(path, json={**request, 'baseHash':'0'*64}).status_code == 422 + assert client.post(path, json={**request, 'endBeat':1000}).status_code == 422 + assert client.post(path, json=request, headers={'Authorization':'Bearer '+'b'*32}).status_code == 404 + reply = client.post(path, json=request) + assert reply.status_code == 200, reply.text + child = reply.json() + assert child['id'] != parent['id'] + assert client.post(path, json=request).json()['id'] == child['id'] + parent_folder = next((worker_root/parent['id']).glob('*/map')) + before = chart_identity(parent_folder) + original = read_json(parent_folder/'EasyStandard.dat') + client.headers['Authorization'] = 'Bearer '+'w'*32 + run_once(client, worker_root) + client.headers.update(user) + result = client.get('/api/jobs/'+child['id']).json() + assert chart_identity(parent_folder) == before + if invalid_candidate: + assert result['result']['status'] == 'invalid' + assert result['result']['artifacts'] == [] + return + assert result['result']['status'] == 'playtest_candidate', result + revised = next((worker_root/child['id']).glob('*/map')) + assert chart_identity(parent_folder) == before + assert (revised/'HardStandard.dat').read_bytes() == (parent_folder/'HardStandard.dat').read_bytes() + chart = read_json(revised/'EasyStandard.dat') + assert [n for n in chart['colorNotes'] if n['b'] >= 8] == [n for n in original['colorNotes'] if n['b'] >= 8] + assert not validate_package(revised).errors + provenance = read_json(revised/'_beatforge/provenance.json') + assert provenance['humanPlaytests'] == [] and provenance['releaseGate'] == {} + assert 'reviews' not in provenance + assert client.get(f"/api/jobs/{child['id']}/artifacts").status_code == 200 diff --git a/tests/test_connector_store.py b/tests/test_connector_store.py new file mode 100644 index 0000000..90d9488 --- /dev/null +++ b/tests/test_connector_store.py @@ -0,0 +1,73 @@ +import pytest +from beatforge.connector_store import ConnectorStore + + +def test_requests_are_owner_scoped_and_survive_restart(tmp_path): + path = tmp_path / 'jobs.sqlite3' + store = ConnectorStore(path) + job = store.submit('alice', 'one', {'brief': 'lighter verses'}) + assert ConnectorStore(path).get('alice', job['id']) == job + assert store.submit('alice', 'one', {'brief': 'lighter verses'}) == job + with pytest.raises(ValueError): + store.submit('alice', 'one', {'brief': 'different'}) + with pytest.raises(KeyError): + store.get('bob', job['id']) + assert store.claim('bob') is None + + +def test_stale_worker_cannot_overwrite_retry_or_cancel(tmp_path, monkeypatch): + import beatforge.connector_store as module + clock = [100.0] + monkeypatch.setattr(module.time, 'time', lambda: clock[0]) + store = ConnectorStore(tmp_path / 'jobs.sqlite3') + job = store.submit('alice', 'one', {}) + first = store.claim('alice', lease_seconds=10) + assert store.claim('alice') is None + clock[0] = 111 + second = store.claim('alice') + assert second['attempts'] == 2 + with pytest.raises(ValueError): + store.finish('alice', job['id'], first['leaseToken'], {}) + store.cancel('alice', job['id']) + with pytest.raises(ValueError): + store.finish('alice', job['id'], second['leaseToken'], {}) + assert store.get('alice', job['id'])['state'] == 'cancelled' + + +def test_successful_result_and_no_lease_disclosure(tmp_path): + store = ConnectorStore(tmp_path / 'jobs.sqlite3') + job = store.submit('alice', 'one', {}) + lease = store.claim('alice') + store.heartbeat('alice', job['id'], lease['leaseToken']) + result = store.finish('alice', job['id'], lease['leaseToken'], {'qaErrors': 0}) + assert result['state'] == 'completed' + assert result['result'] == {'qaErrors': 0} + assert 'leaseToken' not in store.get('alice', job['id']) + + +def test_parallel_workers_claim_a_job_once(tmp_path): + from concurrent.futures import ThreadPoolExecutor + store = ConnectorStore(tmp_path / 'jobs.sqlite3') + job = store.submit('alice', 'one', {}) + with ThreadPoolExecutor(max_workers=5) as pool: + results = list(pool.map(lambda _: store.claim('alice'), range(5))) + leases = [result for result in results if result] + assert len(leases) == 1 + assert leases[0]['id'] == job['id'] + assert leases[0]['attempts'] == 1 + + +def test_crashed_worker_attempts_are_bounded(tmp_path, monkeypatch): + import beatforge.connector_store as module + clock = [100.0] + monkeypatch.setattr(module.time, 'time', lambda: clock[0]) + store = ConnectorStore(tmp_path / 'jobs.sqlite3') + job = store.submit('alice', 'one', {}) + for attempt in range(3): + lease = store.claim('alice', lease_seconds=10) + assert lease['attempts'] == attempt + 1 + clock[0] += 11 + assert store.claim('alice') is None + assert store.get('alice', job['id'])['state'] == 'failed' + with pytest.raises(ValueError): + store.finish('alice', job['id'], lease['leaseToken'], {}) diff --git a/tests/test_connector_uploads.py b/tests/test_connector_uploads.py new file mode 100644 index 0000000..1be3629 --- /dev/null +++ b/tests/test_connector_uploads.py @@ -0,0 +1,124 @@ +import hashlib +import asyncio + +import pytest +from fastapi.testclient import TestClient + +from beatforge.connector import Principal, create_app +from beatforge.connector_store import ConnectorStore + + +def test_stalled_audio_upload_releases_writer_and_removes_partial_file(tmp_path, monkeypatch): + from fastapi import HTTPException, Request + from beatforge.connector_uploads import Uploads + monkeypatch.setattr('beatforge.connector_uploads.UPLOAD_TIMEOUT_SECONDS', 0.02) + store = ConnectorStore(tmp_path/'jobs.db') + uploads = Uploads(store, tmp_path/'uploads') + reservation = store.reserve_upload('alice', 'track.wav', 4) + + async def exercise(): + delivered = False + async def receive(): + nonlocal delivered + if not delivered: + delivered = True + return {'type':'http.request', 'body':b'ab', 'more_body':True} + await asyncio.sleep(10) + request = Request({'type':'http', 'headers':[]}, receive) + with pytest.raises(HTTPException) as failure: + await uploads.receive('alice', reservation['id'], request) + assert failure.value.status_code == 408 + assert store.get_upload('alice', reservation['id'])['state'] == 'pending' + assert not list(uploads.root.iterdir()) + async def retry(): + return {'type':'http.request', 'body':b'data', 'more_body':False} + result = await uploads.receive('alice', reservation['id'], Request({'type':'http','headers':[]}, retry)) + assert result['state'] == 'ready' + assert uploads.path(reservation['id']).read_bytes() == b'data' + asyncio.run(exercise()) + + +@pytest.fixture +def connection(tmp_path): + tokens = {'a'*32: Principal('alice', frozenset({'read', 'generate'})), + 'b'*32: Principal('bob', frozenset({'read', 'generate'})), + 'c'*32: Principal('viewer', frozenset({'read'}))} + with TestClient(create_app(database=tmp_path/'jobs.db', credentials=tokens, hosts=['testserver'])) as client: + client.headers['Authorization'] = 'Bearer '+'a'*32 + yield client + + +def test_upload_is_immutable_and_owner_scoped(connection, tmp_path): + client = connection + reserved = client.post('/api/uploads', json={'filename': 'track.wav', 'size': 4}) + assert reserved.status_code == 200 + upload = reserved.json() + url = upload['uploadPath'] + assert client.put(url, content=b'data', headers={'Authorization': 'Bearer '+'b'*32}).status_code == 404 + response = client.put(url, content=b'data') + assert response.status_code == 200 + assert response.json()['state'] == 'ready' + assert response.json()['sha256'] == hashlib.sha256(b'data').hexdigest() + assert client.put(url, content=b'evil').status_code == 422 + assert (tmp_path/'uploads'/(upload['id']+'.audio')).read_bytes() == b'data' + assert client.get('/api/uploads/'+upload['id'], headers={'Authorization': 'Bearer '+'b'*32}).status_code == 404 + assert client.get('/api/uploads/'+upload['id']).json()['state'] == 'ready' + + +def test_stream_limit_rejects_oversize_and_allows_retry(connection, tmp_path): + upload = connection.post('/api/uploads', json={'filename': 'track.wav', 'size': 4}).json() + assert connection.put(upload['uploadPath'], content=iter([b'012', b'345'])).status_code == 413 + assert not list((tmp_path/'uploads').iterdir()) + assert connection.get('/api/uploads/'+upload['id']).json()['state'] == 'pending' + assert connection.put(upload['uploadPath'], content=iter([b'12'])).status_code == 422 + assert connection.put(upload['uploadPath'], content=b'abcd').status_code == 200 + + +@pytest.mark.parametrize('payload', [ + {'filename': '../track.wav', 'size': 4}, {'filename': 'C:\\track.wav', 'size': 4}, + {'filename': 'track.exe', 'size': 4}, {'filename': 'track.wav', 'size': 0}, + {'filename': 'track.wav', 'size': 67108865}, {'filename': 'track.wav', 'size': 4, 'path': '/etc/passwd'}, +]) +def test_upload_validation(connection, payload): + assert connection.post('/api/uploads', json=payload).status_code == 422 + + +def test_quota_and_permission(connection): + payload = {'filename': 'large.wav', 'size': 64*1024*1024} + assert connection.post('/api/uploads', json=payload, headers={'Authorization': 'Bearer '+'c'*32}).status_code == 403 + for _ in range(4): + assert connection.post('/api/uploads', json=payload).status_code == 200 + assert connection.post('/api/uploads', json=payload).status_code == 422 + + +def test_new_reservation_reclaims_only_expired_pending_quota(connection, tmp_path): + store = ConnectorStore(tmp_path/'jobs.db') + pending = store.reserve_upload('alice', 'unused.wav', 64*1024*1024) + writing = store.reserve_upload('alice', 'active.wav', 64*1024*1024) + ready = store.reserve_upload('alice', 'finished.wav', 64*1024*1024) + fresh = store.reserve_upload('alice', 'fresh.wav', 64*1024*1024) + store.begin_upload('alice', writing['id']) + store.begin_upload('alice', ready['id']) + store.complete_upload('alice', ready['id'], 'a'*64) + with store.connect() as db: + db.execute('UPDATE uploads SET expires=0 WHERE id IN (?,?,?)', + (pending['id'], writing['id'], ready['id'])) + response = connection.post('/api/uploads', json={'filename':'replacement.wav', 'size':64*1024*1024}) + assert response.status_code == 200 + assert connection.get('/api/uploads/'+pending['id']).status_code == 404 + assert store.get_upload('alice', writing['id'])['state'] == 'writing' + assert store.get_upload('alice', ready['id'])['state'] == 'ready' + assert store.get_upload('alice', fresh['id'])['state'] == 'pending' + assert connection.post('/api/uploads', json={'filename':'extra.wav','size':1}).status_code == 422 + + +def test_expired_reservation_and_parallel_writer_denied(connection, tmp_path): + upload = connection.post('/api/uploads', json={'filename': 'track.wav', 'size': 4}).json() + store = ConnectorStore(tmp_path/'jobs.db') + store.begin_upload('alice', upload['id']) + assert connection.put(upload['uploadPath'], content=b'data').status_code == 422 + store.abort_upload('alice', upload['id']) + with store.connect() as db: + db.execute('UPDATE uploads SET expires=0 WHERE id=?', (upload['id'],)) + assert connection.put(upload['uploadPath'], content=b'data').status_code == 422 + assert not list((tmp_path/'uploads').iterdir()) diff --git a/tests/test_connector_worker.py b/tests/test_connector_worker.py new file mode 100644 index 0000000..4c435b9 --- /dev/null +++ b/tests/test_connector_worker.py @@ -0,0 +1,87 @@ +import pytest +from fastapi.testclient import TestClient + +from beatforge.connector import Principal, create_app +from beatforge.connector_store import ConnectorStore +from beatforge.connector_worker import run_once, validate_gateway + + +def setup_job(tmp_path): + credentials = {'a'*32: Principal('alice', frozenset({'read', 'generate', 'edit'})), + 'w'*32: Principal('alice', frozenset({'worker'})), + 'b'*32: Principal('bob', frozenset({'worker', 'read'}))} + app = create_app(database=tmp_path/'jobs.db', credentials=credentials, hosts=['testserver']) + client = TestClient(app) + client.headers['Authorization'] = 'Bearer '+'a'*32 + upload = client.post('/api/uploads', json={'filename': 'song.wav', 'size': 4}).json() + assert client.put(upload['uploadPath'], content=b'data').status_code == 200 + request = {'uploadId': upload['id'], 'idempotencyKey': 'one', 'title': 'Synthetic test', + 'mappingPlan': {'brief': 'lighter verses, no bombs'}} + response = client.post('/api/jobs', json=request) + assert response.status_code == 200, response.text + assert response.json()['workerOnline'] is False + assert client.post('/api/jobs', json=request).json()['id'] == response.json()['id'] + return client, response.json(), request + + +def test_worker_lease_audio_finish_and_scopes(tmp_path): + client, job, _ = setup_job(tmp_path) + with client: + assert client.post('/api/worker/claim').status_code == 403 + assert client.post('/api/worker/claim', headers={'Authorization': 'Bearer '+'b'*32}).json()['job'] is None + worker = {'Authorization': 'Bearer '+'w'*32} + claimed = client.post('/api/worker/claim', headers=worker).json()['job'] + assert claimed['id'] == job['id'] + assert client.get('/api/capabilities').json()['generationAvailable'] is True + worker['X-BeatForge-Lease'] = claimed['leaseToken'] + assert client.get(f"/api/worker/jobs/{job['id']}/audio", headers=worker).content == b'data' + assert client.post(f"/api/jobs/{job['id']}/cancel").status_code == 200 + assert client.get(f"/api/worker/jobs/{job['id']}/audio", headers=worker).status_code == 422 + assert client.post(f"/api/worker/jobs/{job['id']}/finish", headers=worker, json={'status': 'playtest_candidate'}).status_code == 422 + + +def test_outbound_worker_calls_pipeline_with_editorial_plan(tmp_path, monkeypatch): + client, job, _ = setup_job(tmp_path) + received = {} + def pipeline(**kwargs): + received.update(kwargs) + assert kwargs['audio'].read_bytes() == b'data' + return {'status': 'needs_anchors'} + monkeypatch.setattr('beatforge.connector_worker.run_premium_pipeline', pipeline) + with client: + client.headers['Authorization'] = 'Bearer '+'w'*32 + assert run_once(client, tmp_path/'worker') is True + assert run_once(client, tmp_path/'worker') is False + assert received['mapping_plan']['noBombs'] is True + assert received['mapping_plan']['verseDensity'] < 1 + assert received['allow_unconfirmed'] is False + result = ConnectorStore(tmp_path/'jobs.db').get('alice', job['id']) + assert result['state'] == 'completed' + assert result['result']['status'] == 'needs_anchors' + assert result['result']['humanPlaytestRequired'] is True + assert 'audio' not in result['result'] + + +def test_worker_refuses_corrupted_audio_before_pipeline(tmp_path, monkeypatch): + client, job, _ = setup_job(tmp_path) + (tmp_path/'uploads'/(job['request']['uploadId']+'.audio')).write_bytes(b'evil') + monkeypatch.setattr('beatforge.connector_worker.run_premium_pipeline', lambda **kwargs: pytest.fail('Corrupt input reached mapper')) + with client: + client.headers['Authorization'] = 'Bearer '+'w'*32 + with pytest.raises(ValueError, match='integrity'): + run_once(client, tmp_path/'worker') + assert ConnectorStore(tmp_path/'jobs.db').get('alice', job['id'])['state'] == 'failed' + + +def test_job_quota_does_not_break_idempotency(tmp_path): + client, job, request = setup_job(tmp_path) + with client: + assert client.post('/api/jobs', json={**request, 'idempotencyKey': 'two'}).status_code == 200 + assert client.post('/api/jobs', json={**request, 'idempotencyKey': 'three'}).status_code == 422 + assert client.post('/api/jobs', json=request).json()['id'] == job['id'] + + +@pytest.mark.parametrize('url', ['http://example.com', 'https://user:secret@example.com', 'https://example.com/path', 'https://example.com?token=abc']) +def test_worker_rejects_unsafe_gateway_urls(url): + with pytest.raises(ValueError): + validate_gateway(url) diff --git a/tests/test_demo_assets.py b/tests/test_demo_assets.py new file mode 100644 index 0000000..1d31e80 --- /dev/null +++ b/tests/test_demo_assets.py @@ -0,0 +1,141 @@ +"""The public demo must show the packaged charts and the real decoded audio.""" + +from __future__ import annotations + +import hashlib +import json +import sys +import zipfile +from pathlib import Path + +import numpy as np +import soundfile as sf +import pytest + +ROOT = Path(__file__).resolve().parents[1] +DEMO = ROOT / "web" / "assets" / "demo" +SCRIPTS = ROOT / "skills" / "beat-saber-mapping" / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +from validate_map import validate_package +from timing_export import ExportClock, project_map_timing, project_plan_timing +from mapping_plan import build_section_plan +from beatforge.preview import chart_identity, waveform + + +def test_demo_audio_decodes_to_the_advertised_30_seconds() -> None: + preview = json.loads((DEMO / "preview.json").read_text()) + audio, sample_rate = sf.read(DEMO / "song.ogg", always_2d=True) + assert sample_rate == 44100 + assert audio.shape == (30 * 44100, 2) + assert preview["duration"] == len(audio) / sample_rate == 30.0 + assert float(np.sqrt(np.mean(audio ** 2))) > 0.02 + assert float(np.max(np.abs(audio))) < 1 + assert len(preview["waveform"]) >= 1000 + assert max(preview["waveform"]) > 0.2 + assert len(set(preview["waveform"])) > 100 + duration, peaks = waveform(DEMO / "song.ogg") + assert preview["duration"] == duration + # libsndfile/Vorbis builds can straddle a four-decimal rounding boundary. + # Keep every bin aligned and permit only one stored quantization step. + np.testing.assert_allclose(preview["waveform"], peaks, rtol=0, atol=0.00010001) + assert preview["provenance"]["audio"]["sha256"] == hashlib.sha256((DEMO / "song.ogg").read_bytes()).hexdigest() + + +def test_demo_zip_and_displayed_charts_pass_canonical_validation(tmp_path: Path) -> None: + preview = json.loads((DEMO / "preview.json").read_text()) + with zipfile.ZipFile(DEMO / "map.zip") as archive: + assert archive.read("song.ogg") == (DEMO / "song.ogg").read_bytes() + archive.extractall(tmp_path) + info = json.loads(archive.read("Info.dat")) + refs = info["_difficultyBeatmapSets"][0]["_difficultyBeatmaps"] + assert len(refs) == 5 + for entry in refs: + name = entry["_difficulty"] + chart = json.loads(archive.read(entry["_beatmapFilename"])) + assert preview["charts"][name] == chart + assert preview["summary"]["difficulties"][name]["notes"] == len(chart["colorNotes"]) > 0 + for field, collection in (("arcs", "sliders"), ("chains", "burstSliders"), ("bombs", "bombNotes"), ("walls", "obstacles")): + assert preview["summary"]["difficulties"][name][field] == len(chart[collection]) + assert preview["chart"] == preview["charts"]["Hard"] + assert preview["difficulty"] == "Hard" + validation = validate_package(tmp_path) + assert not validation.errors, validation.to_dict() + assert validation.status == "playtest_candidate" + assert preview["qa"]["errors"] == [] + assert preview["qa"]["metrics"]["totals"] == validation.to_dict()["metrics"]["totals"] + assert preview["chartHash"] == chart_identity(tmp_path) + assert preview["audioHash"] == hashlib.sha256((tmp_path / "song.ogg").read_bytes()).hexdigest() + + +def test_demo_timing_provenance_describes_the_authored_score_honestly() -> None: + preview = json.loads((DEMO / "preview.json").read_text()) + with zipfile.ZipFile(DEMO / "map.zip") as archive: + analysis = json.loads(archive.read("_beatforge/analysis.json")) + provenance = json.loads(archive.read("_beatforge/provenance.json")) + assert analysis["source"] == preview["source"] == "synthetic-authored-score" + assert analysis["timingEvidence"]["authoredSampleClock"] is True + assert analysis["timingEvidence"]["trackerAnalysisPerformed"] is False + assert analysis["timingEvidence"]["humanListeningVerified"] is False + for event in analysis["events"]: + assert event["sample"] == round(event["beat"] * 60 / analysis["bpm"] * analysis["sampleRate"]) + assert 0 <= event["sample"] < round(analysis["durationSeconds"] * analysis["sampleRate"]) + assert provenance["officialCorpusUsed"] is False + assert provenance["humanPlaytests"] == [] + assert provenance["releaseGate"]["fullSpeedVrPlaytest"] is False + assert provenance["releaseGate"]["freshSightRead"] is False + assert provenance["releaseGate"]["structuralInspection"] is True + assert preview["sections"][0]["startBeat"] == 0 + assert preview["sections"][-1]["endBeat"] == 60 + + +def test_demo_grid_and_raw_sections_are_ready_for_safe_local_revision() -> None: + preview = json.loads((DEMO / "preview.json").read_text()) + with zipfile.ZipFile(DEMO / "map.zip") as archive: + analysis = json.loads(archive.read("_beatforge/analysis.json")) + grid = json.loads(archive.read("_beatforge/beat_grid.json")) + sections = json.loads(archive.read("_beatforge/sections.json")) + plan = json.loads(archive.read("_beatforge/mapping_plan.json")) + report = json.loads(archive.read("_beatforge/choreography_report.json")) + provenance = json.loads(archive.read("_beatforge/provenance.json")) + assert analysis["durationSamples"] == 30 * 44100 + assert grid["beats"] == analysis["beatGrid"] + assert grid["tempoRegions"] == analysis["tempoRegions"] + assert grid["sampleRate"] == analysis["sampleRate"] == 44100 + rows = grid["beats"] + assert len(rows) >= 60 + for row in rows: + assert row["sample"] == round(row["beat"] * 60 / analysis["bpm"] * analysis["sampleRate"]) + assert 0 <= row["sample"] < analysis["durationSamples"] + assert row["timeSeconds"] == row["sample"] / analysis["sampleRate"] + assert all(right["sample"] > left["sample"] and right["beat"] > left["beat"] for left, right in zip(rows, rows[1:])) + assert "controls" not in sections + assert all("authoredStartBeat" not in section and "density" not in section for section in sections["sections"]) + expected = project_plan_timing(build_section_plan(analysis, sections, plan["controls"]), analysis) + assert plan == expected == report["mappingPlan"] == provenance["mappingPlan"] == preview["mappingPlan"] + assert preview["sections"] == plan["sections"] + assert plan["sections"][-1]["endSeconds"] == preview["duration"] + + +def test_demo_gameplay_and_hold_endpoints_use_the_exported_sample_clock_once() -> None: + preview = json.loads((DEMO / "preview.json").read_text()) + with zipfile.ZipFile(DEMO / "map.zip") as archive: + analysis = json.loads(archive.read("_beatforge/analysis.json")) + clock = ExportClock(analysis) + for event in analysis["events"]: + exported_seconds = clock.beat(event["beat"]) * 60 / analysis["bpm"] + assert abs(exported_seconds * analysis["sampleRate"] - event["sample"]) < 0.001 + for chart in preview["charts"].values(): + assert chart["customData"]["_beatforgeTiming"]["clock"] == "constant-bpm-sample-projection" + assert chart["bpmEvents"] == [] + for collection in ("colorNotes", "bombNotes", "sliders", "burstSliders"): + for item in chart.get(collection, []): + for field in ("b", "tb"): + if field not in item: + continue + sample = item[field] * 60 / analysis["bpm"] * analysis["sampleRate"] + assert abs(sample - round(sample)) < 0.001 + assert 0 <= sample < analysis["durationSamples"] + with pytest.raises(ValueError, match="already been projected"): + project_map_timing(chart, analysis) diff --git a/tests/test_feedback_guidance.py b/tests/test_feedback_guidance.py new file mode 100644 index 0000000..e716b6b --- /dev/null +++ b/tests/test_feedback_guidance.py @@ -0,0 +1,52 @@ +from beatforge.feedback_guidance import derive_feedback_adjustment, qa_next_actions + + +def report(chart, tags=(), **changes): + return {"schemaVersion": 1, "chartHash": chart, "tester": "local", "difficulty": "Expert", "source": "human", "tags": list(tags), "createdAt": "2026-09-12T01:00:00Z", **changes} + + +def test_duplicates_and_sections_do_not_inflate_readiness(): + records = [report("one", ["too_dense"]) for _ in range(6)] + records.append(report("one", ["too_dense"], startBeat=4, endBeat=8)) + result = derive_feedback_adjustment(records, {}) + assert result["evidenceCount"] == 1 + assert result["ready"] is False + assert result["changedFields"] == {} + + +def test_latest_report_replaces_old_vote_and_other_testers_are_excluded(): + records = [report("one", ["too_dense"]), report("one", ["too_sparse"], createdAt="2026-09-12T02:00:00Z"), report("two", ["too_sparse"]), report("three", ["too_sparse"]), report("four", ["too_dense"], tester="other")] + result = derive_feedback_adjustment(records, {"density": 1.0}, difficulty="Expert") + assert result["ready"] and result["evidenceCount"] == 3 + assert result["suggestedPlan"]["density"] == 1.15 + + +def test_suggestion_is_bounded_and_preserves_manual_controls_and_input(): + plan = {"density": 0.55, "style": "tech", "noBombs": True, "dominantInstrument": "bass", "sectionOverrides": [{"id": "section-001", "density": 1.4}]} + records = [report(str(i), ["too_dense", "awkward", "repetitive"]) for i in range(3)] + result = derive_feedback_adjustment(records, plan) + suggested = result["suggestedPlan"] + assert suggested["density"] == 0.5 + assert suggested["intensity"] == 0.9 + assert suggested["candidateCount"] == 3 + assert all(suggested[key] == plan[key] for key in ("style", "noBombs", "dominantInstrument", "sectionOverrides")) + assert plan["density"] == 0.55 + + +def test_automated_or_wrong_difficulty_reports_are_not_human_evidence(): + records = [report(str(i), ["too_dense"], source="automated") for i in range(3)] + records += [report("easy", ["too_dense"], difficulty="Easy")] + assert derive_feedback_adjustment(records, {}, difficulty="Expert")["evidenceCount"] == 0 + + +def test_mixed_signals_make_no_density_adjustment_and_timing_gets_review_action(): + records = [report("one", ["too_dense"]), report("two", ["too_sparse"]), report("three", ["off_beat"])] + result = derive_feedback_adjustment(records, {}) + assert result["changedFields"] == {} + assert result["nextActions"] and "timing" in result["nextActions"][0] + + +def test_qa_prioritizes_known_blockers_and_never_marks_a_chart_playtested(): + actions = qa_next_actions({"errors": [{"code": "HANDCLAP", "message": "Hands collide"}]}, {"status": "needs_anchors"}) + assert [item["action"] for item in actions] == ["repair_structure", "review_timing"] + assert qa_next_actions({"errors": []}, {"status": "timing_verified"})[0]["action"] == "human_playtest" diff --git a/tests/test_gateway_isolation.py b/tests/test_gateway_isolation.py new file mode 100644 index 0000000..3b54d38 --- /dev/null +++ b/tests/test_gateway_isolation.py @@ -0,0 +1,35 @@ +"""The hosted process must not import the local game or audio runtime.""" +import os +from pathlib import Path +import subprocess +import sys + + +def test_gateway_runs_without_local_mapping_dependencies(tmp_path): + root = Path(__file__).resolve().parents[1] + code = ''' +import importlib.abc +import sys +from pathlib import Path + +class BlockLocalRuntime(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): + blocked = {'numpy', 'scipy', 'torch', 'librosa', 'soundfile', 'UnityPy', 'ortools', + 'beatforge.api', 'beatforge.premium', 'beatforge.preview', 'beatforge.learning'} + if any(fullname == name or fullname.startswith(name + '.') for name in blocked): + raise ImportError('Gateway imported local runtime: ' + fullname) + +sys.meta_path.insert(0, BlockLocalRuntime()) +from fastapi.testclient import TestClient +from beatforge.connector import create_app +from beatforge.connector_auth import Principal +app = create_app(database=Path(sys.argv[1]), credentials={'a' * 32: Principal('test', frozenset({'read'}))}) +with TestClient(app, base_url='http://localhost') as client: + assert client.get('/health').status_code == 200 + assert client.get('/api/jobs', headers={'Authorization': 'Bearer ' + 'a' * 32}).status_code == 200 + assert client.get('/api/jobs').status_code == 401 +''' + env = {**os.environ, 'PYTHONPATH': str(root / 'src')} + result = subprocess.run([sys.executable, '-c', code, str(tmp_path / 'jobs.sqlite3')], + cwd=root, env=env, capture_output=True, text=True, timeout=30) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/tests/test_job_lifecycle.py b/tests/test_job_lifecycle.py new file mode 100644 index 0000000..1190eff --- /dev/null +++ b/tests/test_job_lifecycle.py @@ -0,0 +1,216 @@ +"""Studio contracts, failed gates, and recovery without a real game installation.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from beatforge import api + + +@pytest.fixture +def studio(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(api, "JOBS_DIR", tmp_path / "jobs") + monkeypatch.setattr(api, "METADATA_DIR", tmp_path / "metadata") + monkeypatch.setattr(api, "IMPORTS_DIR", tmp_path / "imports") + monkeypatch.setattr(api, "find_custom_levels", lambda: None) + monkeypatch.setattr(api, "_run_job", api._run_job) + def forbid_install(*args, **kwargs): + raise AssertionError("This test must not install a map") + monkeypatch.setattr(api, "_install_pack_to_custom_levels", forbid_install) + return TestClient(api.app) + + +def saved_job(identifier: str, state: str = "needs_anchors", **fields) -> Path: + api._write_status(identifier, {"id": identifier, "status": state, "stages": [], **fields}) + directory = api._job_dir(identifier) + (directory / "input.wav").write_bytes(b"saved-audio") + return directory + + +def test_browser_metadata_and_creative_plan_reach_generation(studio, monkeypatch: pytest.MonkeyPatch) -> None: + metadata_id = "0123456789abcdef0123" + api.METADATA_DIR.mkdir() + cover = api.METADATA_DIR / "cover.png" + cover.write_bytes(b"cover") + (api.METADATA_DIR / f"{metadata_id}.json").write_text(json.dumps({"_cachePath": str(cover)})) + observed = {} + def pipeline(**kwargs): + observed.update(kwargs) + return {"status": "needs_anchors", "payload": {}} + monkeypatch.setattr(api, "run_premium_pipeline", pipeline) + response = studio.post("/api/generate", files={"audio": ("song.wav", b"RIFF", "audio/wav")}, data={ + "metadataId": metadata_id, + "mappingPlan": json.dumps({"brief": "Bass led", "density": 0.8}), + "difficulties": "Easy,Hard", + }) + assert response.status_code == 200 + job = api._read_status(response.json()["id"]) + assert job["metadataId"] == metadata_id + assert observed["cover"] == cover + assert observed["mapping_plan"]["density"] == 0.8 + assert observed["mapping_plan"]["brief"] == "Bass led" + assert observed["difficulties"] == ["Easy", "Hard"] + assert observed["allow_unconfirmed"] is False + + +@pytest.mark.parametrize("plan", ["not-json", "[]", '{"density": 900}', '{"noWalls": "false"}']) +def test_invalid_mapping_plan_is_rejected_before_creating_job(studio, plan: str) -> None: + response = studio.post("/api/generate", files={"audio": ("song.wav", b"RIFF", "audio/wav")}, data={"mappingPlan": plan}) + assert response.status_code == 400 + assert not api.JOBS_DIR.exists() + + +@pytest.mark.parametrize("anchors,expected", [ + ([{"beat": 0, "sample": 0}, {"beat": 1, "sample": 22050}], [0, 22050]), + ([{"beat": 0, "timeSeconds": 0}, {"beat": 1, "timeSeconds": 0.5}], [0, 22050]), +]) +def test_both_anchor_formats_are_normalized_and_queued(studio, monkeypatch: pytest.MonkeyPatch, anchors, expected) -> None: + directory = saved_job("anchors") + scheduled = [] + monkeypatch.setattr(api, "_run_job", scheduled.append) + response = studio.post("/api/jobs/anchors/anchors", json={"anchors": anchors}) + assert response.status_code == 200 + assert scheduled == ["anchors"] + assert [item["sample"] for item in json.loads((directory / "anchors.json").read_text())["anchors"]] == expected + + +@pytest.mark.parametrize("anchor", [ + {"beat": 1, "sample": -1}, {"beat": "nan", "sample": 1}, + {"beat": 1, "timeSeconds": "inf"}, {"beat": "bad", "sample": 1}, + {"beat": 1, "sample": 2.5}, {"beat": 1, "timeSeconds": None}, +]) +def test_bad_anchors_return_actionable_400(studio, anchor) -> None: + directory = saved_job("badanchors") + response = studio.post("/api/jobs/badanchors/anchors", json={"anchors": [{"beat": 0, "sample": 0}, anchor]}) + assert response.status_code == 400 + assert "anchor" in response.json()["detail"].lower() + assert not (directory / "anchors.json").exists() + assert api._read_status("badanchors")["status"] == "needs_anchors" + + +@pytest.mark.parametrize("report", [None, "{broken", {}, {"status": "invalid", "errors": [], "warnings": []}, {"status": "invalid", "errors": ["collision"], "warnings": []}]) +def test_failed_or_missing_qa_never_packages_or_installs(studio, monkeypatch: pytest.MonkeyPatch, report) -> None: + directory = saved_job("failedqa", "queued") + reports = directory / "map" / "_beatforge" + reports.mkdir(parents=True) + (directory / "map" / "Info.dat").write_text("{}") + if report is not None: + (reports / "qa_report.json").write_text(report if isinstance(report, str) else json.dumps(report)) + (reports / "analysis.json").write_text('{"status":"timing_verified"}') + monkeypatch.setattr(api, "run_premium_pipeline", lambda **kwargs: {"status": "invalid"}) + api._run_job("failedqa") + status = api._read_status("failedqa") + assert status["status"] == status["localStatus"] == "invalid" + assert status["qa"]["errors"] > 0 + assert not status.get("installed") + assert not (directory / "map.zip").exists() + assert studio.get("/api/jobs/failedqa/download").status_code == 409 + assert studio.post("/api/jobs/failedqa/install").status_code == 409 + + +def test_download_rechecks_report_even_if_saved_status_claims_ready(studio) -> None: + directory = saved_job("staleready", "review_required", localStatus="playtest_candidate") + (directory / "map.zip").write_bytes(b"old-package") + assert studio.get("/api/jobs/staleready/download").status_code == 409 + + +def test_startup_marks_abandoned_workers_without_deleting_old_history(studio) -> None: + running = saved_job("running", "running") + cancelled = saved_job("cancelled", "cancelling") + (cancelled / "cancel.requested").touch() + ready = saved_job("oldready", "review_required", createdAt=1) + os.utime(ready, (1, 1)) + api._on_startup() + assert api._read_status("running")["status"] == "interrupted" + assert api._read_status("cancelled")["status"] == "cancelled" + assert api._read_status("oldready")["status"] == "review_required" + assert (running / "input.wav").read_bytes() == b"saved-audio" + assert ready.is_dir() + history = studio.get("/api/jobs").json() + assert history["total"] == 3 + assert history["jobs"][-1]["id"] == "oldready" + assert all(job["canRetry"] for job in history["jobs"]) + + +def test_retry_preserves_confirmed_inputs_without_stale_output_or_evidence(studio, monkeypatch: pytest.MonkeyPatch) -> None: + original = saved_job("original", "interrupted", title="Song", seed=7, difficulties=["Hard"], mappingPlan={"density": 0.8}, metadataId="track", engine="premium") + (original / "anchors.json").write_text('{"anchors":[]}') + (original / "approved_palette.json").write_text('{"approved":true}') + (original / "playtests.json").write_text('[{"passed":true}]') + (original / "map.zip").write_bytes(b"stale") + scheduled = [] + monkeypatch.setattr(api, "_run_job", scheduled.append) + result = studio.post("/api/jobs/original/retry") + assert result.status_code == 200 + identifier = result.json()["id"] + assert identifier != "original" + assert scheduled == [identifier] + retry = api._job_dir(identifier) + for name in ("input.wav", "anchors.json", "approved_palette.json"): + assert (retry / name).read_bytes() == (original / name).read_bytes() + assert not (retry / "map.zip").exists() + assert not (retry / "playtests.json").exists() + status = api._read_status(identifier) + assert status["retryOf"] == "original" + assert status["mappingPlan"] == {"density": 0.8} + assert status["difficulties"] == ["Hard"] + assert status["seed"] == 7 + assert status["metadataId"] == "track" + assert api._read_status("original")["status"] == "interrupted" + + +def test_active_and_section_revision_jobs_cannot_plain_retry(studio) -> None: + saved_job("active", "running") + saved_job("revision", "error", revisionOf="original") + assert studio.post("/api/jobs/active/retry").status_code == 409 + assert studio.post("/api/jobs/revision/retry").status_code == 409 + assert studio.get("/api/jobs/revision").json()["canRetry"] is False + + +def test_cancelling_queued_job_never_starts_pipeline(studio, monkeypatch: pytest.MonkeyPatch) -> None: + saved_job("queued", "queued") + def forbidden(**kwargs): + pytest.fail("Cancelled job started generation") + monkeypatch.setattr(api, "run_premium_pipeline", forbidden) + assert studio.post("/api/jobs/queued/cancel").json()["status"] == "cancelled" + api._run_job("queued") + assert api._read_status("queued")["status"] == "cancelled" + assert studio.post("/api/jobs/queued/cancel").status_code == 409 + + +def test_running_cancel_is_forwarded_and_cannot_publish_output(studio, monkeypatch: pytest.MonkeyPatch) -> None: + saved_job("working", "queued") + def pipeline(**kwargs): + kwargs["progress"]("mapping", "working") + assert studio.post("/api/jobs/working/cancel").json()["status"] == "cancelling" + assert kwargs["cancel_requested"]() is True + raise api.PipelineCancelled("stopped") + monkeypatch.setattr(api, "run_premium_pipeline", pipeline) + api._run_job("working") + status = studio.get("/api/jobs/working").json() + assert status["status"] == "cancelled" + assert status["localStatus"] is None + assert status["canRetry"] is True + assert status["canCancel"] is False + assert not (api._job_dir("working") / "map.zip").exists() + + +def test_failed_retest_removes_old_pass_until_that_tester_passes_again(studio) -> None: + directory = saved_job("retest", "review_required", localStatus="playtest_candidate", difficulties=["Easy"]) + evidence = {"difficulty": "Easy", "speed": "full", "tester": "mapper", "passed": True} + response = studio.post("/api/jobs/retest/playtests", json=evidence) + assert response.json()["releaseGate"]["allFiveFullSpeed"] is True + evidence["passed"] = False + response = studio.post("/api/jobs/retest/playtests", json=evidence) + assert response.json()["releaseGate"]["allFiveFullSpeed"] is False + assert response.json()["releaseGate"]["unresolvedFailures"] == 1 + evidence["passed"] = True + response = studio.post("/api/jobs/retest/playtests", json=evidence) + assert response.json()["releaseGate"]["allFiveFullSpeed"] is True + assert response.json()["releaseGate"]["unresolvedFailures"] == 0 + assert len(json.loads((directory / "playtests.json").read_text())) == 3 diff --git a/tests/test_learning.py b/tests/test_learning.py new file mode 100644 index 0000000..2eec481 --- /dev/null +++ b/tests/test_learning.py @@ -0,0 +1,211 @@ +"""Human learning data stays local, attributable, bounded, and separate from clearance.""" + +from __future__ import annotations + +import hashlib +import json +import math +import struct +import subprocess +import sys +import wave +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from beatforge import api, learning + + +@pytest.fixture +def studio(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient: + monkeypatch.setattr(api, "JOBS_DIR", tmp_path / "jobs") + monkeypatch.setattr(learning, "LEARNING_FILE", tmp_path / "learning.json") + return TestClient(api.app) + + +def create_map(identifier: str, variant: int = 0, audio_variant: int = 0, **status_fields) -> Path: + api._write_status(identifier, {"status": "review_required", "localStatus": "playtest_candidate", "difficulties": ["Easy", "Hard"], "mappingPlan": {"density": 1.0}, "privateCorpus": "DO-NOT-EXPORT-CORPUS", "inputPath": "C:/private/song.wav", **status_fields}) + folder = api._job_dir(identifier) / "map" + folder.mkdir() + info = {"_version": "2.1.0", "_songName": "Original test score", "_songFilename": "song.wav", "_beatsPerMinute": 120, "_songTimeOffset": 0, "_difficultyBeatmapSets": [{"_beatmapCharacteristicName": "Standard", "_difficultyBeatmaps": []}]} + for difficulty in ("Easy", "Hard"): + name = f"{difficulty}Standard.dat" + chart = {"version": "3.3.0", "colorNotes": [{"b": 1, "x": variant % 4, "y": 0, "c": 0, "d": 1}, {"b": 3, "x": 3, "y": 1, "c": 1, "d": 0}], "bombNotes": [], "obstacles": [], "sliders": [], "burstSliders": []} + (folder / name).write_text(json.dumps(chart)) + info["_difficultyBeatmapSets"][0]["_difficultyBeatmaps"].append({"_difficulty": difficulty, "_beatmapFilename": name, "_noteJumpMovementSpeed": 12, "_noteJumpStartBeatOffset": 0.5}) + (folder / "Info.dat").write_text(json.dumps(info)) + with wave.open(str(folder / "song.wav"), "wb") as audio: + audio.setnchannels(1) + audio.setsampwidth(2) + audio.setframerate(8000) + audio.writeframes(b"".join(struct.pack(" dict: + return {"jobId": job, "difficulty": "Hard", "tester": "local", "ratings": {"flow": 3, "overall": 4}, "tags": ["too_dense"], "notes": "Less dense chorus, please.", **changes} + + +def test_feedback_hashes_actual_chart_and_audio_without_creating_headset_evidence(studio) -> None: + folder = create_map("one") + before_status = api._read_status("one") + before_provenance = (folder / "_beatforge" / "provenance.json").read_bytes() + response = studio.post("/api/feedback", json=feedback(startBeat=2, endBeat=4)) + assert response.status_code == 200 + record = response.json()["feedback"] + assert record["source"] == "human" + assert len(record["chartHash"]) == len(record["mapHash"]) == 64 + assert record["audioHash"] == hashlib.sha256((folder / "song.wav").read_bytes()).hexdigest() + assert record["startBeat"] == 2 + assert record["endBeat"] == 4 + assert api._read_status("one") == before_status + assert (folder / "_beatforge" / "provenance.json").read_bytes() == before_provenance + assert not (api._job_dir("one") / "playtests.json").exists() + summary = studio.get("/api/learning").json() + assert summary["feedbackCount"] == 1 + assert summary["recentFeedback"][0]["id"] == record["id"] + assert summary["modelRegistry"]["automaticTraining"] is False + + +@pytest.mark.parametrize("changes", [ + {"ratings": {"flow": 6}}, {"ratings": {"overall": "5"}}, {"ratings": {"flow": True}}, + {"ratings": {"other": 3}}, {"tags": ["unknown"]}, {"startBeat": 2}, + {"startBeat": 4, "endBeat": 2}, {"startBeat": 2, "endBeat": 1000}, + {"startBeat": "NaN", "endBeat": 4}, {"tester": " "}, + {"source": "human"}, {"chartHash": "forged"}, + {"ratings": {}, "tags": [], "notes": ""}, {"difficulty": "Expert"}, +]) +def test_malformed_feedback_never_writes_learning(studio, changes) -> None: + create_map("one") + response = studio.post("/api/feedback", json=feedback(**changes)) + assert response.status_code == 422 + assert not learning.LEARNING_FILE.exists() + + +def test_feedback_and_presets_survive_a_new_python_process(studio) -> None: + create_map("one") + assert studio.post("/api/feedback", json=feedback()).status_code == 200 + preset = studio.post("/api/presets", json={"name": "My smooth maps", "mappingPlan": {"density": 0.85, "style": "flow"}}) + assert preset.status_code == 200 + assert preset.json()["mappingPlan"]["density"] == 0.85 + script = "import json,sys; from pathlib import Path; from beatforge import learning; learning.LEARNING_FILE=Path(sys.argv[1]); value=learning._read_store(); print(json.dumps({'feedbackCount':len(value['feedback']),'preset':value['presets'][0]}))" + result = subprocess.run([sys.executable, "-c", script, str(learning.LEARNING_FILE)], capture_output=True, text=True, check=True, timeout=30) + recovered = json.loads(result.stdout) + assert recovered["feedbackCount"] == 1 + assert recovered["preset"] == preset.json() + assert studio.get("/api/learning").json()["presets"] == [preset.json()] + + +def test_suggestions_use_distinct_human_charts_and_leave_plan_unchanged(studio) -> None: + for index in range(3): + create_map(f"chart{index}", variant=index) + assert studio.post("/api/feedback", json=feedback(f"chart{index}")).status_code == 200 + plan = {"density": 1.0, "candidateCount": 2} + response = studio.post("/api/learning/suggestions", json={"mappingPlan": plan, "tester": "local", "difficulty": "Hard"}) + assert response.status_code == 200 + result = response.json() + assert result["ready"] is True + assert result["evidenceCount"] == 3 + assert result["suggestedPlan"]["density"] < 1.0 + assert all(api._read_status(f"chart{index}")["mappingPlan"]["density"] == 1.0 for index in range(3)) + other = studio.post("/api/learning/suggestions", json={"mappingPlan": plan, "tester": "someone else"}).json() + assert other["ready"] is False + assert other["evidenceCount"] == 0 + + +def test_comparison_requires_identical_audio_and_different_selected_chart(studio) -> None: + create_map("one", variant=0) + create_map("otheraudio", variant=1, audio_variant=20) + create_map("identical", variant=0) + request = {"preferredJob": "one", "alternateJob": "otheraudio", "difficulty": "Hard", "tester": "local", "notes": "Preferred flow"} + assert studio.post("/api/comparisons", json=request).status_code == 409 + request["alternateJob"] = "identical" + assert studio.post("/api/comparisons", json=request).status_code == 409 + hard = api._job_dir("identical") / "map" / "HardStandard.dat" + hard.write_text(json.dumps(json.loads(hard.read_text()), sort_keys=True, indent=4)) + assert studio.post("/api/comparisons", json=request).status_code == 409 + # Changing a different difficulty does not make Hard a meaningful A/B pair. + easy = api._job_dir("identical") / "map" / "EasyStandard.dat" + chart = json.loads(easy.read_text()) + chart["colorNotes"][0]["x"] = 2 + easy.write_text(json.dumps(chart)) + assert studio.post("/api/comparisons", json=request).status_code == 409 + assert not learning.LEARNING_FILE.exists() + + +def test_latest_comparison_replaces_the_same_testers_vote_in_either_direction(studio) -> None: + create_map("one", variant=0) + create_map("two", variant=1) + request = {"preferredJob": "one", "alternateJob": "two", "difficulty": "Hard", "tester": "local", "notes": "First pass"} + first = studio.post("/api/comparisons", json=request).json() + assert first["comparisonCount"] == 1 + assert first["replaced"] is False + request.update(preferredJob="two", alternateJob="one", notes="Changed my preference") + second = studio.post("/api/comparisons", json=request).json() + assert second["comparisonCount"] == 1 + assert second["replaced"] is True + assert second["comparison"]["id"] == first["comparison"]["id"] + assert second["comparison"]["preferredJob"] == "two" + request["tester"] = "second player" + assert studio.post("/api/comparisons", json=request).json()["comparisonCount"] == 2 + + +def test_revision_acceptance_is_explicit_and_originals_or_failed_revisions_are_rejected(studio) -> None: + create_map("one") + create_map("revision", variant=1, revisionOf="one", revision={"difficulty": "Hard", "startBeat": 2, "endBeat": 4, "seed": 4, "baseHash": "a" * 64}) + before = api._read_status("revision") + assert studio.get("/api/learning").json()["acceptedRevisionCount"] == 0 + assert studio.post("/api/jobs/one/accept-revision", json={"tester": "local"}).status_code == 409 + response = studio.post("/api/jobs/revision/accept-revision", json={"tester": "local", "notes": "Keep this passage"}) + assert response.status_code == 200 + record = response.json()["acceptedRevision"] + assert record["parentJob"] == "one" + assert record["revision"]["startBeat"] == 2 + assert record["source"] == "human" + assert api._read_status("revision") == before + assert not (api._job_dir("revision") / "playtests.json").exists() + qa = api._job_dir("revision") / "map" / "_beatforge" / "qa_report.json" + qa.write_text('{"status":"invalid","errors":["collision"],"warnings":[]}') + assert studio.post("/api/jobs/revision/accept-revision", json={"tester": "local"}).status_code == 409 + + +def test_export_contains_feedback_preferences_and_accepted_metadata_without_source_files(studio) -> None: + create_map("one") + create_map("two", variant=1, revisionOf="one", revision={"difficulty": "Hard", "startBeat": 2, "endBeat": 4}) + studio.post("/api/feedback", json=feedback()) + studio.post("/api/comparisons", json={"preferredJob": "two", "alternateJob": "one", "difficulty": "Hard"}) + studio.post("/api/jobs/two/accept-revision", json={"notes": "Keep it"}) + result = studio.get("/api/learning/export") + assert result.status_code == 200 + exported = result.json() + assert exported["schemaVersion"] == 1 + assert exported["containsAudio"] is False + assert exported["containsOfficialCorpus"] is False + assert len(exported["feedback"]) == len(exported["preferences"]) == len(exported["acceptedRevisions"]) == 1 + for forbidden in ("DO-NOT-EXPORT-CORPUS", "C:/private", "colorNotes", "inputPath", "RIFF"): + assert forbidden not in result.text + + +@pytest.mark.parametrize("broken", ["{broken", '{"schemaVersion":2}', '{"schemaVersion":1,"feedback":"oops","comparisons":[],"presets":[],"acceptedRevisions":[]}']) +def test_unreadable_learning_file_is_preserved(studio, broken) -> None: + learning.LEARNING_FILE.write_text(broken) + assert studio.get("/api/learning").status_code == 503 + assert studio.post("/api/presets", json={"name": "Preset", "mappingPlan": {}}).status_code == 503 + assert learning.LEARNING_FILE.read_text() == broken + + +def test_concurrent_preset_saves_do_not_lose_records(studio) -> None: + def save(index): + return studio.post("/api/presets", json={"name": f"Preset {index}", "mappingPlan": {}}).status_code + with ThreadPoolExecutor(max_workers=5) as pool: + assert list(pool.map(save, range(12))) == [200] * 12 + saved = json.loads(learning.LEARNING_FILE.read_text()) + assert len(saved["presets"]) == 12 + assert len({item["id"] for item in saved["presets"]}) == 12 + assert not list(learning.LEARNING_FILE.parent.glob("learning.*.tmp")) diff --git a/tests/test_mapping_plan.py b/tests/test_mapping_plan.py new file mode 100644 index 0000000..295a446 --- /dev/null +++ b/tests/test_mapping_plan.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +import json +from pathlib import Path +import sys +import threading + +import pytest + +from beatforge.mapping_plan import build_section_plan, normalize_mapping_plan + +SCRIPTS = Path(__file__).resolve().parents[1] / "skills" / "beat-saber-mapping" / "scripts" +sys.path.insert(0, str(SCRIPTS)) +import choreography +from timing_export import ExportClock, project_map_timing, project_plan_timing + + +def analysis_fixture() -> tuple[dict, dict]: + analysis = { + "bpm": 120.0, "durationSeconds": 32.0, + "events": [{"beat": i * 0.5, "strength": (i % 5 + 1) / 5.0, "layer": "mix"} for i in range(4, 128)], + } + sections = {"source": "fixture", "sections": [ + {"label": "verse 1", "startBeat": 0, "endBeat": 32, "intensity": 0.5}, + {"label": "chorus 1", "startBeat": 32, "endBeat": 48, "intensity": 0.8}, + {"label": "chorus 2", "startBeat": 48, "endBeat": 64, "intensity": 0.8}, + ]} + return analysis, sections + + +def test_supported_brief_has_explicit_precedence_and_roundtrips(): + result = normalize_mapping_plan({"brief": "Flowing, sparse verses, bigger chorus, no bombs; follow the bass", "chorusDensity": 1.1}) + assert result["noBombs"] is True + assert result["verseDensity"] == 0.75 + assert result["chorusDensity"] == 1.1 + assert result["style"] == "flow" + assert result["dominantInstrument"] == "bass" + assert normalize_mapping_plan(json.loads(json.dumps(result))) == result + + +@pytest.mark.parametrize("value", [ + {"density": 2}, {"density": True}, {"density": float("nan")}, + {"noBombs": "false"}, {"candidateCount": 0}, {"schemaVersion": 2}, + {"dominantInstrument": "rain"}, {"style": "unsafe"}, {"typo": 1}, + {"sectionOverrides": [{"id": "x"}, {"id": "x"}]}, +]) +def test_invalid_controls_are_rejected(value): + with pytest.raises(ValueError): + normalize_mapping_plan(value) + + +def test_section_override_preserves_other_sections_and_repeated_motif(): + analysis, sections = analysis_fixture() + initial = build_section_plan(analysis, sections, {}) + changed = build_section_plan(analysis, sections, {"sectionOverrides": [{"id": "section-001", "density": 0.5}]}) + assert changed["sections"][0] == initial["sections"][0] + assert changed["sections"][2] == initial["sections"][2] + assert changed["sections"][1]["density"] == 0.5 + assert changed["sections"][1]["motifId"] == changed["sections"][2]["motifId"] == "chorus" + with pytest.raises(ValueError, match="unknown section"): + build_section_plan(analysis, sections, {"sectionOverrides": [{"id": "missing"}]}) + + +def test_density_changes_real_intent_selection_without_changing_recovery_limits(): + analysis, sections = analysis_fixture() + original_recovery = choreography.CONFIGS["Expert"].recovery_beats + sparse = choreography.select_intents(analysis, build_section_plan(analysis, sections, {"density": 0.5}), "Expert") + dense = choreography.select_intents(analysis, build_section_plan(analysis, sections, {"density": 1.5}), "Expert") + assert len(sparse) < len(dense) + assert choreography.CONFIGS["Expert"].recovery_beats == original_recovery + assert all(right["beat"] - left["beat"] >= choreography.CONFIGS["Expert"].peak_min_gap for left, right in zip(dense, dense[1:])) + + +def test_dominant_instrument_selects_its_actual_stem_accents(): + analysis, sections = analysis_fixture() + for index, event in enumerate(analysis["events"]): + event["stemEnergy"] = {"bass": 1.0 if index % 2 else 0.0, "drums": 0.0 if index % 2 else 1.0} + event["strength"] = 0.5 + bass = choreography.select_intents(analysis, build_section_plan(analysis, sections, {"dominantInstrument": "bass"}), "Normal") + drums = choreography.select_intents(analysis, build_section_plan(analysis, sections, {"dominantInstrument": "drums"}), "Normal") + assert [item["beat"] for item in bass] != [item["beat"] for item in drums] + + +def test_repeated_chorus_reuses_local_pose_preferences(): + analysis, sections = analysis_fixture() + # Identical sound at the same local beat of two chorus instances. + analysis["events"] = [{"beat": beat, "strength": 3.0} for beat in (34, 50)] + planned = build_section_plan(analysis, sections, {"density": 1.5}) + intents = [item for item in choreography.select_intents(analysis, planned, "Hard") if item["beat"] in (34, 50)] + assert len(intents) == 2 + domains = [choreography._pose_domain(intent, 0, 0, index, 120, choreography.CONFIGS["Hard"].recovery_beats) for index, intent in enumerate(intents)] + shapes = [[(pose["head"]["x"], pose["head"]["y"], pose["head"]["d"]) for pose in domain] for domain in domains] + assert shapes[0] == shapes[1] + + +def simple_map(notes): + return {"version": "3.3.0", "colorNotes": notes, "bombNotes": [], "obstacles": [], "sliders": [], "burstSliders": []} + + +def test_high_corpus_score_cannot_select_an_unsafe_candidate(): + good = simple_map([{"b": 4, "x": 0, "y": 0, "c": 0, "d": 1, "a": 0}]) + bad = simple_map([ + {"b": 4, "x": 0, "y": 1, "c": 0, "d": 5, "a": 0}, + {"b": 4, "x": 2, "y": 0, "c": 1, "d": 4, "a": 0}, + ]) + safe_rank = choreography.rank_choreography_candidate(good, "Expert", 120, {"score": 0}, {}, 1) + unsafe_rank = choreography.rank_choreography_candidate(bad, "Expert", 120, {"score": 100}, {}, 1) + assert safe_rank["eligible"] + assert not unsafe_rank["eligible"] + assert choreography.select_ranked_candidate([{"index": 0, **unsafe_rank}, {"index": 1, **safe_rank}])["index"] == 1 + with pytest.raises(ValueError, match="safety gates"): + choreography.select_ranked_candidate([{"index": 0, **unsafe_rank}]) + + +def test_corpus_ranking_changes_the_actual_winner(): + chart = simple_map([{"b": 4, "x": 0, "y": 0, "c": 0, "d": 1, "a": 0}]) + candidates = [{"index": i, **choreography.rank_choreography_candidate(chart, "Expert", 120, {"score": score}, {}, 1)} for i, score in enumerate((20, 90))] + assert choreography.select_ranked_candidate(candidates)["index"] == 1 + + +def test_premium_plan_generates_a_valid_accessible_chart(tmp_path): + analysis, sections = analysis_fixture() + # Keep the real solver fixture small; the unit rank test covers multiple candidates. + analysis["durationSeconds"] = 8.0 + analysis["events"] = [event for event in analysis["events"] if event["beat"] < 16] + sections["sections"] = [{"label": "verse", "startBeat": 0, "endBeat": 16, "intensity": 0.5}] + maps, report = choreography.generate_all(analysis, sections, 123, tmp_path / "missing.sqlite", ("Easy",), mapping_plan={"candidateCount": 1, "noBombs": True, "noWalls": True}) + chart = maps["Easy"] + assert chart["colorNotes"] + assert chart["bombNotes"] == chart["obstacles"] == [] + assert report["candidateRanking"]["Easy"]["candidates"][0]["eligible"] + assert report["mappingPlan"]["sections"][0]["id"] == "section-000" + + +def test_premium_cancellation_stops_a_real_spawned_mapper(tmp_path, monkeypatch): + from beatforge import premium + script = tmp_path / "generate_map.py" + script.write_text("import sys, time\nprint('BEATFORGE_PROGRESS\\trunning\\tready', file=sys.stderr, flush=True)\ntime.sleep(30)\n", encoding="utf-8") + monkeypatch.setattr(premium, "SCRIPTS", tmp_path) + cancelled = threading.Event() + def progress(stage, detail, **kwargs): + if stage == "running": + cancelled.set() + with pytest.raises(premium.PipelineCancelled): + premium.run_premium_pipeline(audio=tmp_path / "song.wav", output=tmp_path / "out", title="Test", artist="Test", mapper="Test", seed=1, anchors=None, palette=None, progress=progress, cancel_requested=cancelled.is_set) + + +def test_export_uses_actual_offset_and_variable_tempo_for_notes_holds_and_lights(): + # Beat zero is 0.25 seconds into the song. The second bar doubles in duration. + analysis = {"bpm": 120, "sampleRate": 44100, "beatGrid": [ + {"beat": 0, "sample": 11025}, {"beat": 4, "sample": 99225}, + {"beat": 8, "sample": 275625}, + ]} + chart = simple_map([{"b": 6, "x": 0, "y": 0, "c": 0, "d": 1, "a": 0}]) + chart["sliders"] = [{"b": 3, "tb": 5}] + chart["burstSliders"] = [{"b": 4, "tb": 5}] + chart["obstacles"] = [{"b": 3, "d": 2}] + chart["basicBeatmapEvents"] = [{"b": 6, "et": 1, "i": 5, "f": 1.0}] + chart["lightColorEventBoxGroups"] = [{"b": 4, "g": 0, "e": [{"f": {"b": 1}, "e": [{"b": 1, "c": 0}]}]}] + exported = project_map_timing(chart, analysis) + assert exported["colorNotes"][0]["b"] == 8.5 + assert exported["sliders"][0] == {"b": 3.5, "tb": 6.5} + assert exported["burstSliders"][0] == {"b": 4.5, "tb": 6.5} + assert exported["obstacles"][0] == {"b": 3.5, "d": 3.0} + assert exported["basicBeatmapEvents"][0]["b"] == 8.5 + box = exported["lightColorEventBoxGroups"][0] + assert box["b"] == 4.5 + assert box["e"][0]["e"][0]["b"] == 2 + assert box["e"][0]["f"]["b"] == 1 + assert chart["colorNotes"][0]["b"] == 6 # authored input was preserved + with pytest.raises(ValueError, match="already been projected"): + project_map_timing(exported, analysis) + + +def test_exported_section_boundaries_match_notes_for_preview_and_revision(): + analysis = {"bpm": 120, "sampleRate": 44100, "beatGrid": [{"beat": 0, "sample": 11025}, {"beat": 8, "sample": 187425}]} + plan = {"sections": [{"id": "section-001", "startBeat": 2, "endBeat": 4}]} + projected = project_plan_timing(plan, analysis) + section = projected["sections"][0] + assert section["startBeat"] == ExportClock(analysis).beat(2) + assert section["endBeat"] == ExportClock(analysis).beat(4) + assert section["startSeconds"] == 1.25 + assert section["authoredStartBeat"] == 2 + assert plan["sections"][0]["startBeat"] == 2 diff --git a/tests/test_preview.py b/tests/test_preview.py new file mode 100644 index 0000000..e1bc781 --- /dev/null +++ b/tests/test_preview.py @@ -0,0 +1,335 @@ +"""Preview decoding and isolated revision contracts using actual audio and map files.""" + +from __future__ import annotations + +import copy +import hashlib +import json +from pathlib import Path +import sys +from zipfile import ZipFile + +from fastapi import HTTPException +from fastapi.testclient import TestClient +import numpy as np +import pytest +import soundfile as sf + +from beatforge import api, preview + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "skills" / "beat-saber-mapping" / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) +import choreography +from validate_map import validate_package + + +def write_json(path: Path, payload) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + + +def note(beat: float, direction: int = 1, x: int = 0, color: int = 0) -> dict: + return {"b": beat, "x": x, "y": 0 if direction == 1 else 1, "c": color, "d": direction, "a": 0} + + +def chart(notes: list[dict]) -> dict: + return {"version": "3.3.0", "colorNotes": notes, "bombNotes": [], "obstacles": [], "sliders": [], "burstSliders": [], "basicBeatmapEvents": [], "bpmEvents": [], "customData": {"untouched": "source metadata"}} + + +@pytest.fixture +def studio(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(api, "JOBS_DIR", tmp_path / "jobs") + monkeypatch.setattr(api, "find_custom_levels", lambda: None) + def forbidden_install(*args, **kwargs): + raise AssertionError("Preview and revision tests must never install a map") + monkeypatch.setattr(api, "_install_pack_to_custom_levels", forbidden_install) + return TestClient(api.app) + + +@pytest.fixture +def saved_map(studio) -> Path: + folder = api._job_dir("original") / "map" + folder.mkdir(parents=True) + with ZipFile(ROOT / "web" / "assets" / "demo" / "map.zip") as archive: + info = json.loads(archive.read("Info.dat")) + (folder / "cover.png").write_bytes(archive.read("cover.png")) + info["_songName"] = "Revision fixture" + info["_songFilename"] = "song.ogg" + info["_difficultyBeatmapSets"][0]["_difficultyBeatmaps"] = [entry for entry in info["_difficultyBeatmapSets"][0]["_difficultyBeatmaps"] if entry["_difficulty"] in ("Easy", "Hard")] + write_json(folder / "Info.dat", info) + for difficulty in ("Easy", "Hard"): + notes = [note(beat, 1 if index % 2 == 0 else 0) for index, beat in enumerate((2, 4, 6, 8, 10, 12))] + if difficulty == "Hard": + notes.append(note(14, 1)) + write_json(folder / f"{difficulty}Standard.dat", chart(notes)) + samples = np.zeros((44100 * 8, 2), dtype=np.float32) + # Distinct stereo pulses prove the preview decodes samples, rather than fake bars. + samples[11025:11525, 0] = 0.25 + samples[22050:22550, 1] = -0.75 + sf.write(folder / "song.ogg", samples, 44100, format="OGG", subtype="VORBIS") + analysis = { + "schemaVersion": 1, "status": "timing_verified", "bpm": 120, "sampleRate": 44100, + "durationSeconds": 8, "durationSamples": len(samples), + "beatGrid": [{"beat": index / 2, "sample": index * 11025, "timeSeconds": index / 4} for index in range(32)], + "events": [{"beat": index / 2, "strength": 3.0, "layer": "mix"} for index in range(4, 31)], + "clickTrackEvidence": {"checkpoints": [{"sample": value, "label": label} for value, label in ((0, "start"), (176400, "middle"), (330750, "end"))]}, + } + write_json(folder / "_beatforge" / "analysis.json", analysis) + write_json(folder / "_beatforge" / "sections.json", {"sections": [{"id": "section-000", "label": "verse", "startBeat": 0, "endBeat": 8, "intensity": 0.5}, {"id": "section-001", "label": "chorus", "startBeat": 8, "endBeat": 16, "intensity": 0.8}]}) + write_json(folder / "_beatforge" / "provenance.json", {"seed": 12, "releaseGate": {"vrPlaytest": True}, "humanPlaytests": [{"passed": True}], "reviews": [{"approved": True}], "vrPlaytestPassed": True}) + qa = validate_package(folder) + assert not qa.errors, qa.to_dict() + write_json(folder / "_beatforge" / "qa_report.json", qa.to_dict()) + api._write_status("original", {"id": "original", "status": "review_required", "localStatus": "playtest_candidate", "title": "Revision fixture", "artist": "Fixture", "difficulties": ["Easy", "Hard"], "installed": False}) + return folder + + +def revision_payload(folder: Path, **changes) -> dict: + return {"difficulty": "Easy", "startBeat": 6, "endBeat": 8, "baseHash": preview.chart_identity(folder), "seed": 123, "mappingPlan": {"candidateCount": 1, "noWalls": True, "noBombs": True}, **changes} + + +@pytest.mark.parametrize("relative", ["../outside.dat", "missing.dat"]) +def test_confined_rejects_escape_and_missing_files(tmp_path, relative): + root = tmp_path / "map" + root.mkdir() + (tmp_path / "outside.dat").write_text("private") + with pytest.raises(HTTPException) as raised: + preview.confined(root, relative) + assert raised.value.status_code == 404 + + +def test_confined_rejects_absolute_external_path_and_symlink(tmp_path): + root = tmp_path / "map" + root.mkdir() + outside = tmp_path / "outside.dat" + outside.write_text("private") + with pytest.raises(HTTPException): + preview.confined(root, str(outside.resolve())) + try: + (root / "linked.dat").symlink_to(outside) + except OSError: + pytest.skip("This Windows account cannot create symlinks") + with pytest.raises(HTTPException): + preview.confined(root, "linked.dat") + + +def test_waveform_reads_real_stereo_amplitudes_in_bounded_bins(tmp_path): + path = tmp_path / "pulses.wav" + samples = np.zeros((800, 2), dtype=np.float32) + samples[105, 0] = 0.25 + samples[505, 1] = -0.75 + sf.write(path, samples, 800, subtype="FLOAT") + duration, peaks = preview.waveform(path, bins=8) + assert duration == 1.0 + assert peaks == [0.0, 0.25, 0.0, 0.0, 0.0, 0.75, 0.0, 0.0] + + +def test_preview_selects_actual_difficulty_and_filters_qa(studio, saved_map): + qa_path = saved_map / "_beatforge" / "qa_report.json" + qa = preview.read_json(qa_path) + qa["warnings"] = [{"file": "EasyStandard.dat", "code": "EASY_ONLY"}, {"file": "HardStandard.dat", "code": "HARD_ONLY"}, {"file": None, "code": "PACKAGE"}] + write_json(qa_path, qa) + response = studio.get("/api/jobs/original/preview?difficulty=Hard") + assert response.status_code == 200 + payload = response.json() + assert payload["difficulty"] == "Hard" + assert len(payload["chart"]["colorNotes"]) == 7 + assert {item["code"] for item in payload["findings"]} == {"HARD_ONLY", "PACKAGE"} + assert payload["difficulties"] == ["Easy", "Hard"] + assert payload["duration"] == 8 + assert max(payload["waveform"]) > 0.5 + assert payload["audioUrl"] == "/api/jobs/original/audio" + assert studio.get("/api/jobs/original/preview?difficulty=ExpertPlus").status_code == 404 + assert studio.get("/api/jobs/original/audio").content == (saved_map / "song.ogg").read_bytes() + + +def test_preview_rejects_info_audio_path_escape(studio, saved_map): + outside = saved_map.parent / "private.ogg" + outside.write_bytes(b"private") + info = preview.read_json(saved_map / "Info.dat") + info["_songFilename"] = "../private.ogg" + write_json(saved_map / "Info.dat", info) + assert studio.get("/api/jobs/original/preview").status_code == 404 + assert studio.get("/api/jobs/original/audio").status_code == 404 + + +def test_waveform_cache_invalidates_when_audio_changes(saved_map): + first = preview.preview_payload(saved_map) + sf.write(saved_map / "song.ogg", np.zeros(44100, dtype=np.float32), 44100, format="OGG", subtype="VORBIS") + second = preview.preview_payload(saved_map) + assert first["duration"] == 8 and second["duration"] == 1 + assert max(second["waveform"]) == 0 + + +def test_corrupt_matching_hash_waveform_cache_is_recomputed(saved_map): + audio_hash = hashlib.sha256((saved_map / "song.ogg").read_bytes()).hexdigest() + write_json(saved_map / "_beatforge" / "preview-waveform.json", {"audioHash": audio_hash, "duration": 8, "waveform": [float("nan")]}) + payload = preview.preview_payload(saved_map) + assert payload["duration"] == 8 + assert len(payload["waveform"]) > 1 + assert all(np.isfinite(value) for value in payload["waveform"]) + assert max(payload["waveform"]) > 0.5 + + +def test_stale_revision_hash_does_not_create_a_child(studio, saved_map): + request = revision_payload(saved_map) + existing = preview.read_json(saved_map / "EasyStandard.dat") + existing["customData"]["changedAfterPreview"] = True + write_json(saved_map / "EasyStandard.dat", existing) + response = studio.post("/api/jobs/original/revise", json=request) + assert response.status_code == 409 + assert [folder.name for folder in api.JOBS_DIR.iterdir()] == ["original"] + + +def test_audio_change_invalidates_revision_base_hash(studio, saved_map): + request = revision_payload(saved_map) + sf.write(saved_map / "song.ogg", np.zeros(44100, dtype=np.float32), 44100, format="OGG", subtype="VORBIS") + response = studio.post("/api/jobs/original/revise", json=request) + assert response.status_code == 409 + assert [folder.name for folder in api.JOBS_DIR.iterdir()] == ["original"] + + +def test_half_open_merge_preserves_every_outside_object_and_custom_metadata(): + original = chart([note(1), note(4), note(5), note(8), note(10)]) + original["obstacles"] = [{"b": 0, "d": 1, "x": 0, "w": 1, "h": 3}, {"b": 5, "d": 1, "x": 3, "w": 1, "h": 3}, {"b": 9, "d": 1, "x": 0, "w": 1, "h": 3}] + original["sliders"] = [{"b": 1, "tb": 2}, {"b": 5, "tb": 6}, {"b": 9, "tb": 10}] + original["basicBeatmapEvents"] = [{"b": 1, "i": 1}, {"b": 4, "i": 2}, {"b": 8, "i": 3}] + candidate = chart([note(0, x=3), note(4, x=1), note(7, x=1), note(8, x=3)]) + candidate["obstacles"] = [{"b": 6, "d": 1, "x": 2, "w": 1, "h": 3}] + candidate["sliders"] = [{"b": 5, "tb": 7}] + candidate["basicBeatmapEvents"] = [{"b": 4, "i": 7}, {"b": 8, "i": 9}] + before = copy.deepcopy(original) + merged = preview.merge_section(original, candidate, 4, 8) + assert original == before + for collection in preview.COLLECTIONS: + expected = [item for item in before.get(collection, []) if item["b"] < 4 or item["b"] >= 8] + actual = [item for item in merged.get(collection, []) if item["b"] < 4 or item["b"] >= 8] + assert actual == expected + assert merged["customData"] == original["customData"] + assert [item["x"] for item in merged["colorNotes"] if 4 <= item["b"] < 8] == [1, 1] + assert next(item for item in merged["colorNotes"] if item["b"] == 8)["x"] == 0 + + +@pytest.mark.parametrize("collection,object_", [ + ("sliders", {"b": 3, "tb": 5}), ("burstSliders", {"b": 6, "tb": 9}), + ("obstacles", {"b": 3, "d": 2}), ("obstacles", {"b": 7, "d": 2}), +]) +@pytest.mark.parametrize("on_candidate", [False, True]) +def test_crossing_holds_and_walls_are_rejected_on_either_side(collection, object_, on_candidate): + original, candidate = chart([]), chart([]) + (candidate if on_candidate else original)[collection] = [object_] + with pytest.raises(ValueError, match="cuts through"): + preview.merge_section(original, candidate, 4, 8) + + +def test_crossing_nested_light_event_is_rejected(): + original = chart([]) + original["lightColorEventBoxGroups"] = [{"b": 7, "g": 1, "e": [{"e": [{"b": 2, "c": 0}]}]}] + with pytest.raises(ValueError, match="cuts through|timed|light"): + preview.merge_section(original, chart([]), 4, 8) + + +def test_existing_crossing_hold_refuses_revision_before_copy(studio, saved_map): + path = saved_map / "EasyStandard.dat" + original = preview.read_json(path) + original["sliders"] = [{"b": 5, "tb": 7}] + write_json(path, original) + response = studio.post("/api/jobs/original/revise", json=revision_payload(saved_map)) + assert response.status_code == 422 + assert [folder.name for folder in api.JOBS_DIR.iterdir()] == ["original"] + + +def test_source_bpm_events_require_explicit_conversion_before_revision(studio, saved_map): + path = saved_map / "EasyStandard.dat" + original = preview.read_json(path) + original["bpmEvents"] = [{"b": 8, "m": 60}] + write_json(path, original) + response = studio.post("/api/jobs/original/revise", json=revision_payload(saved_map)) + assert response.status_code in (409, 422) + assert [folder.name for folder in api.JOBS_DIR.iterdir()] == ["original"] + + +def test_revision_range_cannot_extend_beyond_audio_duration(studio, saved_map): + before = (saved_map / "EasyStandard.dat").read_bytes() + response = studio.post("/api/jobs/original/revise", json=revision_payload(saved_map, startBeat=0, endBeat=16.01)) + assert response.status_code == 422 + assert "beyond the song" in response.json()["detail"] + assert [folder.name for folder in api.JOBS_DIR.iterdir()] == ["original"] + assert (saved_map / "EasyStandard.dat").read_bytes() == before + + +def test_revision_creates_child_preserves_original_and_other_difficulty(studio, saved_map, monkeypatch): + before = {path.relative_to(saved_map).as_posix(): path.read_bytes() for path in saved_map.rglob("*") if path.is_file()} + replacement = chart([note(6, 1, x=1)]) + monkeypatch.setattr(choreography, "generate_all", lambda *args, **kwargs: ({"Easy": replacement}, {"fixture": "safe section"})) + response = studio.post("/api/jobs/original/revise", json=revision_payload(saved_map)) + assert response.status_code == 200 + child_id = response.json()["id"] + assert child_id != "original" + status = api._read_status(child_id) + assert status["revisionOf"] == "original" + assert status["localStatus"] == "playtest_candidate" + assert status["installed"] is False + child = api._job_dir(child_id) / "map" + for relative, data in before.items(): + assert (saved_map / relative).read_bytes() == data + assert (child / "HardStandard.dat").read_bytes() == before["HardStandard.dat"] + original_chart = json.loads(before["EasyStandard.dat"]) + revised_chart = preview.read_json(child / "EasyStandard.dat") + assert [n for n in revised_chart["colorNotes"] if n["b"] < 6 or n["b"] >= 8] == [n for n in original_chart["colorNotes"] if n["b"] < 6 or n["b"] >= 8] + assert next(n for n in revised_chart["colorNotes"] if n["b"] == 6)["x"] == 1 + provenance = preview.read_json(child / "_beatforge" / "provenance.json") + assert not provenance.get("humanPlaytests") and not provenance.get("reviews") and not provenance.get("vrPlaytestPassed") + assert (api._job_dir(child_id) / "map.zip").is_file() + assert studio.get(f"/api/jobs/{child_id}/download").status_code == 200 + + +def test_boundary_failure_keeps_invalid_child_uninstalled_and_undownloadable(studio, saved_map, monkeypatch): + before = (saved_map / "EasyStandard.dat").read_bytes() + # The new note is safe alone but cannot follow the retained note at beat 6. + replacement = chart([note(6.1, 1)]) + monkeypatch.setattr(choreography, "generate_all", lambda *args, **kwargs: ({"Easy": replacement}, {})) + response = studio.post("/api/jobs/original/revise", json=revision_payload(saved_map, startBeat=6.05)) + assert response.status_code == 200 + child_id = response.json()["id"] + status = api._read_status(child_id) + assert status["status"] == "invalid" and status["localStatus"] == "invalid" + assert status["qa"]["errors"] > 0 + assert status["installed"] is False + assert (saved_map / "EasyStandard.dat").read_bytes() == before + assert not (api._job_dir(child_id) / "map.zip").exists() + assert studio.get(f"/api/jobs/{child_id}/download").status_code == 409 + assert studio.post(f"/api/jobs/{child_id}/install").status_code == 409 + + +def test_real_generator_projection_and_complete_validation_in_revision(studio, saved_map, monkeypatch, tmp_path): + from timing_export import project_map_timing + analysis_path = saved_map / "_beatforge" / "analysis.json" + analysis = preview.read_json(analysis_path) + # A quarter-second origin catches a missing authored-to-audio projection. + for row in analysis["beatGrid"]: + row["sample"] += 11025 + row["timeSeconds"] += 0.25 + analysis["beatGrid"] = [row for row in analysis["beatGrid"] if row["sample"] < analysis["durationSamples"]] + write_json(analysis_path, analysis) + for name in ("EasyStandard.dat", "HardStandard.dat"): + write_json(saved_map / name, project_map_timing(preview.read_json(saved_map / name), analysis)) + real_generate = choreography.generate_all + def isolated_generate(analysis, sections, seed, **kwargs): + return real_generate(analysis, sections, seed, corpus_database=tmp_path / "no-corpus.sqlite", **kwargs) + monkeypatch.setattr(choreography, "generate_all", isolated_generate) + before = (saved_map / "EasyStandard.dat").read_bytes() + response = studio.post("/api/jobs/original/revise", json=revision_payload(saved_map, startBeat=0, endBeat=16)) + assert response.status_code == 200 + child_id = response.json()["id"] + status = api._read_status(child_id) + assert status.get("localStatus") == "playtest_candidate", status + child = api._job_dir(child_id) / "map" + assert not validate_package(child).errors + assert (saved_map / "EasyStandard.dat").read_bytes() == before + notes = preview.read_json(child / "EasyStandard.dat")["colorNotes"] + assert notes + assert all(abs((item["b"] % 1.0) - 0.5) < 1e-6 for item in notes) + assert (api._job_dir(child_id) / "map.zip").is_file() diff --git a/tests/test_studio_experience.py b/tests/test_studio_experience.py new file mode 100644 index 0000000..520d224 --- /dev/null +++ b/tests/test_studio_experience.py @@ -0,0 +1,248 @@ +"""Browser acceptance of real sample playback and isolated local-workflow contracts.""" + +from __future__ import annotations + +import copy +import json +from email.parser import BytesParser +from email.policy import default +from pathlib import Path +from urllib.parse import urlparse + +import pytest + +from test_browser import _playwright_chromium, studio_url + +ROOT = Path(__file__).resolve().parents[1] +DEMO = json.loads((ROOT / "web" / "assets" / "demo" / "preview.json").read_text()) +A = "aaaaaaaaaaaa" +B = "bbbbbbbbbbbb" +C = "cccccccccccc" + + +@pytest.fixture +def browser_page(studio_url): + playwright, browser = _playwright_chromium() + context = browser.new_context(viewport={"width": 1280, "height": 1000}) + page = context.new_page() + errors = [] + page.on("pageerror", lambda error: errors.append(str(error))) + yield page, studio_url + context.close() + browser.close() + playwright.stop() + assert errors == [], errors + + +def job(identifier=A, state="review_required", **extra): + return {"id": identifier, "title": "Saved score", "artist": "Fixture", "status": state, "difficulties": ["Hard"], "localStatus": "playtest_candidate" if state == "review_required" else None, "canRetry": state in {"review_required", "interrupted", "cancelled"}, "canCancel": state in {"running", "queued"}, "mappingPlan": {"density": 0.75, "style": "flow"}, "stages": [], "releaseGate": {"evidenceCount": 0, "localHardGates": True, "aiReleaseRoute": False}, **extra} + + +def preview(identifier=A, notes="Hard"): + result = {key: copy.deepcopy(value) for key, value in DEMO.items() if key not in {"charts", "summary"}} + result.update(jobId=identifier, difficulty="Hard", chart=copy.deepcopy(DEMO["charts"][notes]), difficulties=["Hard"], chartHash=identifier[0] * 64, audioHash="f" * 64, audioUrl="/assets/demo/song.ogg", canRevise=True) + return result + + +def mock_api(page, jobs=None, previews=None, suggestions=None, feedback_error=None): + """Mock service state only; sample bytes and all UI code are served unchanged.""" + jobs = jobs if jobs is not None else {} + previews = previews if previews is not None else {} + calls = [] + suggestions = suggestions or {"ready": False, "evidenceCount": 0, "reasons": ["Rate three distinct charts."], "changedFields": {}, "suggestedPlan": {"density": 1}} + + def respond(route, payload, status=200): + route.fulfill(status=status, content_type="application/json", body=json.dumps(payload)) + + def handler(route): + request = route.request + path = urlparse(request.url).path + body = request.post_data_json if request.method == "POST" and "application/json" in request.headers.get("content-type", "") else None + calls.append((request.method, path, body)) + if path == "/api/jobs": + return respond(route, {"jobs": list(jobs.values()), "total": len(jobs)}) + if path == "/api/learning": + return respond(route, {"feedbackCount": 0, "comparisonCount": 0, "presets": [], "recentFeedback": [], "suggestions": suggestions}) + if path == "/api/learning/suggestions": + return respond(route, suggestions) + if path == "/api/mapping-plan/resolve": + return respond(route, body or {}) + if path == "/api/beat-saber/status": + return respond(route, {"found": False, "path": None}) + if path == "/api/beat-saber/playtest-maps": + return respond(route, {"found": False, "maps": []}) + if path.startswith("/api/settings/providers/"): + return respond(route, {"configured": False, "runnerInstalled": False, "label": "OpenAI Codex", "model": "test-model", "maximumCostUsd": 5}) + if path == "/api/feedback": + return respond(route, {"detail": feedback_error}, 409) if feedback_error else respond(route, {"feedback": body, "feedbackCount": 1}) + if path == "/api/comparisons": + return respond(route, {"comparison": body, "comparisonCount": 1}) + if path.startswith("/api/jobs/"): + parts = path.split("/") + identifier = parts[3] + action = parts[4] if len(parts) > 4 else "" + if action == "preview" and identifier in previews: + return respond(route, previews[identifier]) + if identifier not in jobs: + return respond(route, {"detail": "Run not found"}, 404) + if action == "events": + return route.fulfill(status=200, content_type="text/event-stream", body=f"event: progress\ndata: {json.dumps(jobs[identifier])}\n\nevent: end\ndata: {{}}\n\n") + if action == "retry": + jobs[C] = job(C, "needs_anchors", retryOf=identifier) + return respond(route, {"id": C, "status": "queued", "retryOf": identifier}) + if action == "cancel": + jobs[identifier].update(status="cancelled", canCancel=False, canRetry=True) + return respond(route, {"id": identifier, "status": "cancelled"}) + if not action: + return respond(route, jobs[identifier]) + return respond(route, {"detail": f"Unexpected test request {path}"}, 404) + + page.route("**/api/**", handler) + return calls + + +def test_sample_plays_actual_audio_and_switches_actual_difficulties(browser_page): + page, url = browser_page + calls = mock_api(page) + page.goto(url, wait_until="networkidle") + page.locator("#loadDemo").click() + page.wait_for_function("window.BeatForgePreview.getState()?.notes > 0 && document.querySelector('#previewAudio').duration === 30") + assert page.evaluate("window.BeatForgePreview.getState().notes") == len(DEMO["charts"]["Hard"]["colorNotes"]) + assert page.locator("#previewDifficulty option").all_text_contents() == DEMO["difficulties"] + page.locator("#previewPlay").click() + page.wait_for_function("document.querySelector('#previewAudio').currentTime > 0.15") + page.locator("#previewPlay").click() + assert page.evaluate("document.querySelector('#previewAudio').paused") is True + page.locator("#previewSeek").fill("12") + page.locator("#previewSeek").dispatch_event("input") + page.wait_for_function("Number(document.querySelector('#chartCanvas').dataset.renderedNotes) > 0") + assert float(page.locator("#chartCanvas").get_attribute("data-time")) == pytest.approx(12, abs=0.1) + page.locator("#previewDifficulty").select_option("Easy") + assert page.evaluate("window.BeatForgePreview.getState().notes") == len(DEMO["charts"]["Easy"]["colorNotes"]) + assert page.evaluate("window.BeatForgePreview.getState().time") == pytest.approx(12, abs=0.1) + page.locator("#generate").click() + page.wait_for_function("window.BeatForgeApp.getStudioState().job?.demo === true") + assert "20 notes" in page.locator("#difficultyList").inner_text() + assert page.locator("#download").get_attribute("href").endswith("assets/demo/map.zip") + assert page.evaluate("window.BeatForgeApp.getStudioState().job.releaseGate.evidenceCount") == 0 + assert not any(method == "POST" and path == "/api/generate" for method, path, _ in calls) + + +def test_creative_controls_are_sent_with_the_real_upload(browser_page, monkeypatch): + from beatforge import api + + page, url = browser_page + monkeypatch.setattr(api, "_run_job", lambda identifier: None) + jobs = {A: job(A, "needs_anchors")} + mock_api(page, jobs=jobs) + captured = {} + def capture(route): + request = route.request + message = BytesParser(policy=default).parsebytes(b"Content-Type: " + request.headers["content-type"].encode() + b"\r\nMIME-Version: 1.0\r\n\r\n" + request.post_data_buffer) + for part in message.iter_parts(): + name = part.get_param("name", header="content-disposition") + captured[name] = part.get_payload(decode=True) + route.continue_() + page.route("**/api/generate", capture) + page.goto(url, wait_until="networkidle") + page.locator("#audioFile").set_input_files(ROOT / "web" / "assets" / "demo" / "song.ogg") + page.locator("#creativeBrief").fill("Flowing patterns with no bombs") + page.locator("#planDensity").select_option("0.75") + page.locator("#planInstrument").select_option("drums") + page.locator("#planBombs").select_option("hide") + page.locator('.diff-toggle[data-difficulty="Easy"]').click() + with page.expect_response(lambda response: urlparse(response.url).path == "/api/generate") as generated: + page.locator("#generate").click() + response = generated.value + assert response.status == 200 + identifier = response.json()["id"] + assert (api._job_dir(identifier) / "input.ogg").read_bytes() == (ROOT / "web" / "assets" / "demo" / "song.ogg").read_bytes() + plan = json.loads(captured["mappingPlan"]) + assert plan["density"] == 0.75 + assert plan["dominantInstrument"] == "drums" + assert plan["noBombs"] is True + assert plan["brief"] == "Flowing patterns with no bombs" + assert captured["difficulties"].decode() == "Normal,Hard,Expert,ExpertPlus" + assert captured["engine"] == b"premium" + + +def test_static_page_blocks_full_generation_and_explains_local_studio(browser_page): + page, url = browser_page + page.context.add_init_script("window.BEATFORGE_STATIC = true") + calls = mock_api(page) + page.goto(url, wait_until="networkidle") + page.locator("#audioFile").set_input_files(ROOT / "web" / "assets" / "demo" / "song.ogg") + page.locator("#generate").click() + assert page.locator("#statusTitle").inner_text() == "Open your local Studio to generate" + assert "Start-BeatForge.ps1" in page.locator("#statusDetail").inner_text() + assert page.locator("#feedbackForm").is_hidden() + assert not any(method == "POST" and path == "/api/generate" for method, path, _ in calls) + + +def test_history_restores_selection_retries_saved_input_and_cancels(browser_page): + page, url = browser_page + jobs = {A: job(A, "interrupted"), B: job(B, "running", title="Working score")} + calls = mock_api(page, jobs=jobs) + page.goto(url, wait_until="networkidle") + page.locator(f'[data-open="{A}"]').click() + page.wait_for_function("document.querySelector('#jobLabel').textContent.includes('aaaaaaaaaaaa')") + assert page.locator("#planDensity").input_value() == "0.75" + assert page.evaluate("localStorage.getItem('beatforge.lastJob')") == A + page.reload(wait_until="networkidle") + page.wait_for_function("document.querySelector('#jobLabel').textContent.includes('aaaaaaaaaaaa')") + page.locator(f'[data-retry="{A}"]').click() + page.wait_for_function("document.querySelector('#jobLabel').textContent.includes('cccccccccccc')") + assert jobs[C]["retryOf"] == A + page.locator(f'[data-cancel="{B}"]').click() + page.wait_for_function("!document.querySelector('[data-cancel=bbbbbbbbbbbb]')") + assert jobs[B]["status"] == "cancelled" + posts = [path for method, path, _ in calls if method == "POST"] + assert f"/api/jobs/{A}/retry" in posts + assert f"/api/jobs/{B}/cancel" in posts + assert "/api/generate" not in posts + + +def test_ab_switching_preserves_audio_time_and_saves_explicit_preference(browser_page): + page, url = browser_page + jobs = {A: job(A, summary=DEMO["summary"]), B: job(B, summary=DEMO["summary"])} + previews = {A: preview(A, "Hard"), B: preview(B, "Easy")} + calls = mock_api(page, jobs=jobs, previews=previews) + page.goto(url, wait_until="networkidle") + page.locator(f'[data-open="{A}"]').click() + page.wait_for_function("window.BeatForgePreview.getState()?.jobId === 'aaaaaaaaaaaa'") + page.locator("#compareControls summary").click() + page.locator("#compareJob").select_option(B) + page.locator("#compareLoad").click() + page.wait_for_function("!document.querySelector('#compareB').disabled") + page.locator("#previewSeek").fill("9.5") + page.locator("#previewSeek").dispatch_event("input") + page.locator("#compareB").click() + page.wait_for_function("window.BeatForgePreview.getState()?.jobId === 'bbbbbbbbbbbb' && window.BeatForgePreview.getState().notes === 20 && Math.abs(window.BeatForgePreview.getState().time - 9.5) < 0.1") + assert page.evaluate("window.BeatForgePreview.getState().notes") == len(DEMO["charts"]["Easy"]["colorNotes"]) + page.locator("#preferB").click() + page.wait_for_function("document.querySelector('#feedbackMessage').textContent.includes('Preference for B saved')") + body = next(body for method, path, body in reversed(calls) if method == "POST" and path == "/api/comparisons") + assert body["preferredJob"] == B + assert body["alternateJob"] == A + assert not any(path.endswith("/playtests") for _, path, _ in calls) + + +def test_feedback_error_is_visible_and_suggestions_require_explicit_apply(browser_page): + page, url = browser_page + suggestions = {"ready": True, "evidenceCount": 3, "reasons": ["Three charts were too dense."], "changedFields": {"density": {"from": 1, "to": 0.85}}, "suggestedPlan": {"density": 0.85, "style": "flow"}} + jobs = {A: job(A, summary=DEMO["summary"])} + calls = mock_api(page, jobs=jobs, previews={A: preview()}, suggestions=suggestions, feedback_error="Feedback could not be saved; your previous notes are preserved.") + page.goto(url, wait_until="networkidle") + assert page.locator("#planDensity").input_value() == "auto" + page.wait_for_function("!document.querySelector('#applySuggestion').disabled") + page.locator("#applySuggestion").click() + assert page.locator("#planDensity").input_value() == "0.85" + assert not any(path == "/api/generate" for _, path, _ in calls) + page.locator(f'[data-open="{A}"]').click() + page.wait_for_function("window.BeatForgePreview.getState()?.jobId === 'aaaaaaaaaaaa'") + page.locator("#rating-flow").select_option("3") + page.locator("#feedbackNotes").fill("The chorus felt cramped.") + page.locator('#feedbackForm button[type="submit"]').click() + page.wait_for_function("document.querySelector('#feedbackMessage').textContent.includes('could not be saved')") + assert not any(path.endswith("/playtests") for _, path, _ in calls) + assert page.evaluate("window.BeatForgeApp.getStudioState().job.releaseGate.evidenceCount") == 0 diff --git a/tests/test_webmcp.py b/tests/test_webmcp.py index f864cc4..18973a3 100644 --- a/tests/test_webmcp.py +++ b/tests/test_webmcp.py @@ -1,6 +1,8 @@ """WebMCP challenge contracts for the browser collaboration surface.""" from pathlib import Path +import json +import re from fastapi.testclient import TestClient @@ -23,8 +25,11 @@ def test_webmcp_uses_standard_document_registration_and_named_tools() -> None: "generate_beatmap", "review_current_beatmap", "record_human_playtest", + "get_chart_preview", + "get_learning_summary", + "record_mapping_feedback", ): - assert f'name: "{name}"' in WEBMCP + assert re.search(r'name\s*:\s*"' + re.escape(name) + r'"', WEBMCP) assert "inputSchema" in WEBMCP assert "window.BeatForgeWebMcp" in WEBMCP @@ -49,10 +54,14 @@ def test_webmcp_script_is_served_by_the_studio() -> None: assert "document.modelContext.registerTool" in response.text -def test_demo_is_explicitly_rights_safe_and_not_a_real_download() -> None: +def test_demo_download_contains_real_rights_safe_chart_assets() -> None: assert "Synthetic 30-second groove" in WEB - assert "Browser preview · no ZIP" in WEB + assert "Download sample ZIP" in WEB assert "Human headset evidence is still required" in WEB + demo = json.loads((ROOT / "web" / "assets" / "demo" / "preview.json").read_text()) + assert demo["source"] == "synthetic-authored-score" + assert len(demo["charts"]) == 5 + assert (ROOT / "web" / "assets" / "demo" / "map.zip").is_file() def test_metadata_lookup_is_explicitly_preview_only() -> None: diff --git a/tests/test_worker_reconnect.py b/tests/test_worker_reconnect.py new file mode 100644 index 0000000..5a38e38 --- /dev/null +++ b/tests/test_worker_reconnect.py @@ -0,0 +1,82 @@ +import httpx +import pytest + +from beatforge.connector_worker import run_once, serve +from beatforge.connector_store import ConnectorStore +from test_connector_worker import setup_job + + +def test_transient_download_failure_leaves_job_recoverable(tmp_path, monkeypatch): + client, job, _ = setup_job(tmp_path) + def unavailable(*args, **kwargs): + raise httpx.ReadTimeout('synthetic network outage') + monkeypatch.setattr(client, 'stream', unavailable) + with client: + client.headers['Authorization'] = 'Bearer ' + 'w' * 32 + with pytest.raises(httpx.ReadTimeout): + run_once(client, tmp_path / 'worker') + store = ConnectorStore(tmp_path / 'jobs.db') + saved = store.get('alice', job['id']) + assert saved['state'] == 'running' + assert saved['result'] is None + with store.connect() as db: + db.execute('UPDATE jobs SET lease_until=0 WHERE id=?', (job['id'],)) + reclaimed = store.claim('alice') + assert reclaimed['id'] == job['id'] + assert reclaimed['attempts'] == 2 + + +def test_reconnect_backoff_is_bounded_and_resets_after_success(tmp_path, monkeypatch): + outcomes = [httpx.ConnectError('outage')] * 8 + [True, httpx.ReadTimeout('outage'), KeyboardInterrupt()] + waits = [] + def step(*args): + result = outcomes.pop(0) + if isinstance(result, BaseException): + raise result + return result + monkeypatch.setattr('beatforge.connector_worker.run_once', step) + with pytest.raises(KeyboardInterrupt): + serve(None, tmp_path, sleep=waits.append) + assert waits == [2, 4, 8, 16, 32, 60, 60, 60, 2] + + +def test_lost_finish_response_preserves_committed_result(tmp_path, monkeypatch): + client, job, _ = setup_job(tmp_path) + monkeypatch.setattr('beatforge.connector_worker.run_premium_pipeline', lambda **kwargs: {'status': 'needs_anchors'}) + original = client.post + finishes = [] + def post(url, **kwargs): + response = original(url, **kwargs) + if url.endswith('/finish'): + finishes.append(kwargs['json']) + response.raise_for_status() + raise httpx.ReadTimeout('Response lost after commit') + return response + monkeypatch.setattr(client, 'post', post) + with client: + client.headers['Authorization'] = 'Bearer ' + 'w' * 32 + with pytest.raises(httpx.ReadTimeout): + run_once(client, tmp_path / 'worker') + saved = ConnectorStore(tmp_path / 'jobs.db').get('alice', job['id']) + assert saved['state'] == 'completed' + assert saved['result']['status'] == 'needs_anchors' + assert len(finishes) == 1 + + +@pytest.mark.parametrize('status', [401, 403, 404]) +def test_auth_and_configuration_failures_do_not_retry(tmp_path, monkeypatch, status): + error = httpx.HTTPStatusError('rejected', request=httpx.Request('POST', 'https://example.test'), + response=httpx.Response(status)) + def step(*args): + raise error + monkeypatch.setattr('beatforge.connector_worker.run_once', step) + with pytest.raises(httpx.HTTPStatusError): + serve(None, tmp_path, sleep=lambda delay: pytest.fail('Permanent error retried')) + + +def test_once_does_not_hide_outage(tmp_path, monkeypatch): + def step(*args): + raise httpx.ConnectError('outage') + monkeypatch.setattr('beatforge.connector_worker.run_once', step) + with pytest.raises(httpx.ConnectError): + serve(None, tmp_path, once=True, sleep=lambda delay: pytest.fail('Once retried')) diff --git a/tools/build_demo_assets.py b/tools/build_demo_assets.py new file mode 100644 index 0000000..f773917 --- /dev/null +++ b/tools/build_demo_assets.py @@ -0,0 +1,204 @@ +"""Build an original 30-second score and real, validated browser demo charts. + +This fixture invokes canonical choreography directly with a mathematically +authored sample timeline. It does not pretend to analyze a recording, run the +official-corpus pipeline, use a trained model, or provide headset evidence. +""" + +from __future__ import annotations + +import argparse +import copy +import hashlib +import json +import shutil +import sys +import tempfile +import wave +from pathlib import Path +from typing import Any + +import numpy as np + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "skills" / "beat-saber-mapping" / "scripts" +for directory in (ROOT / "src", SCRIPTS): + if str(directory) not in sys.path: + sys.path.insert(0, str(directory)) + +from choreography import generate_all +from beatforge_core import encode_ogg +from generate_map import DAFT_PUNK_COLOR_SCHEME, info_dat, write_default_cover +from mapping_plan import build_section_plan, normalize_mapping_plan +from timing_export import project_map_timing, project_plan_timing +from validate_map import validate_package +from beatforge.premium import package_map, summarize_map +from beatforge.preview import preview_payload + +SAMPLE_RATE = 44100 +BPM = 120.0 +DURATION = 30.0 +SEED = 20260911 +DIFFICULTIES = ("Easy", "Normal", "Hard", "Expert", "ExpertPlus") + + +def write_json(path: Path, value: Any) -> None: + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + + +def synthesize_score() -> tuple[np.ndarray, dict[str, Any], dict[str, Any]]: + """Synthesize every instrument on the same integer sample clock as the score.""" + length = int(SAMPLE_RATE * DURATION) + stems = {name: np.zeros(length, dtype=np.float64) for name in ("drums", "bass", "piano")} + rng = np.random.default_rng(SEED) + timeline: dict[float, dict[str, Any]] = {} + + def add(beat: float, instrument: str, samples: np.ndarray, strength: float, sustain: float = 0) -> None: + start = round(beat * 60 / BPM * SAMPLE_RATE) + count = min(len(samples), length - start) + if count <= 0: + return + stems[instrument][start:start + count] += samples[:count] + item = timeline.setdefault(beat, {"beat": beat, "snappedBeat": beat, "sample": start, "timeSeconds": start / SAMPLE_RATE, "strength": 0.0, "sustainBeats": 0.0, "instruments": []}) + item["instruments"].append(instrument) + if strength > item["strength"]: + item.update(strength=strength, layer=instrument) + item["sustainBeats"] = max(item["sustainBeats"], sustain) + + for half_beat in range(120): + beat = half_beat / 2 + chorus = 24 <= beat < 40 or 48 <= beat < 58 + if half_beat % 2 == 0: + t = np.arange(round(0.22 * SAMPLE_RATE)) / SAMPLE_RATE + # A pitched kick settles from its transient onto the bass root. + phase = 2 * np.pi * (48 * t + 6.5 * (1 - np.exp(-t * 28))) + kick = np.sin(phase) * np.exp(-t * 21) * 0.65 + add(beat, "drums", kick, 1.8 if beat % 4 == 0 else 1.2) + if int(beat) % 2 == 1: + t = np.arange(round(0.13 * SAMPLE_RATE)) / SAMPLE_RATE + snare = (rng.normal(0, 1, len(t)) * 0.16 + np.sin(2 * np.pi * 185 * t) * 0.12) * np.exp(-t * 28) + add(beat, "drums", snare, 1.4) + if beat >= 8: + t = np.arange(round(0.035 * SAMPLE_RATE)) / SAMPLE_RATE + noise = rng.normal(0, 1, len(t)) + hat = (noise - np.concatenate(([0], noise[:-1]))) * np.exp(-t * 125) * (0.035 if chorus else 0.024) + add(beat, "drums", hat, 0.72 if chorus else 0.5) + if half_beat % 2 == 0 and beat >= 4: + root = (55.0, 65.4064, 48.9994, 58.2705)[int(beat // 8) % 4] + t = np.arange(round(0.31 * SAMPLE_RATE)) / SAMPLE_RATE + bass = (np.sin(2 * np.pi * root * t) + 0.22 * np.sin(2 * np.pi * root * 2 * t)) * np.minimum(t / 0.004, 1) * np.exp(-t * 11) * 0.25 + add(beat, "bass", bass, 1.0, 0.5) + if beat >= 16 and half_beat % (1 if chorus else 4) == 0 and beat < 58: + degree = (0, 7, 12, 3, 10, 7, 3, 12)[half_beat % 8] + frequency = 220 * 2 ** (degree / 12) + t = np.arange(round(0.2 * SAMPLE_RATE)) / SAMPLE_RATE + note = (np.sin(2 * np.pi * frequency * t) + 0.2 * np.sin(2 * np.pi * frequency * 2 * t)) * np.minimum(t / 0.003, 1) * np.exp(-t * 18) * 0.095 + add(beat, "piano", note, 0.88 if chorus else 0.7, 0.25) + + events = [] + for beat, item in sorted(timeline.items()): + start = item["sample"] + energies = {name: float(np.sqrt(np.mean(stem[start:min(start + 2048, length)] ** 2))) for name, stem in stems.items()} + scale = max(energies.values(), default=1.0) or 1.0 + item["stemEnergy"] = {name: round(value / scale, 6) for name, value in energies.items()} + events.append(item) + mix = sum(stems.values()) + mix /= max(1.0, float(np.max(np.abs(mix))) / 0.85) + fade = round(SAMPLE_RATE * 0.3) + mix[-fade:] *= np.linspace(1, 0, fade) + stereo = np.column_stack((mix, mix)).astype(np.float32) + checkpoints = [{"label": label, "beat": beat, "sample": round(beat * 60 / BPM * SAMPLE_RATE), "timeSeconds": beat * 60 / BPM, "source": "synthetic-authored-score"} for label, beat in (("start", 4), ("middle", 30), ("end", 56))] + analysis = { + "schemaVersion": 1, "status": "timing_verified", "source": "synthetic-authored-score", + "sampleRate": SAMPLE_RATE, "durationSeconds": DURATION, "durationSamples": length, "bpm": BPM, + "gridSource": "synthetic-authored-score", + "beatGrid": [ + # The score is authored on half beats, exactly 11025 samples apart. + # Finer decisions interpolate these anchors without accumulated rounding drift. + {"beat": tick / 2, "sample": round(tick / 2 * 60 / BPM * SAMPLE_RATE), "timeSeconds": round(tick / 2 * 60 / BPM * SAMPLE_RATE) / SAMPLE_RATE} + for tick in range(round(DURATION * BPM / 60 * 2)) + ], + "tempoRegions": [{"startBeat": 0, "endBeat": DURATION * BPM / 60, "startSample": 0, "endSample": length, "bpm": BPM}], + "offsetSeconds": 0.0, "events": events, + "timingEvidence": {"method": "Every oscillator and drum is placed at round(beat * 60 / bpm * sampleRate)", "authoredSampleClock": True, "trackerAnalysisPerformed": False, "humanListeningVerified": False}, + "clickTrackEvidence": {"source": "authored-score-checkpoints", "checkpoints": checkpoints}, + } + sections = {"source": "synthetic-authored-score", "sections": [ + {"id": "section-000", "label": "Intro", "startBeat": 0, "endBeat": 8, "intensity": 0.3}, + {"id": "section-001", "label": "Verse", "startBeat": 8, "endBeat": 24, "intensity": 0.5}, + {"id": "section-002", "label": "Chorus", "startBeat": 24, "endBeat": 40, "intensity": 0.82}, + {"id": "section-003", "label": "Verse", "startBeat": 40, "endBeat": 48, "intensity": 0.5}, + {"id": "section-004", "label": "Chorus", "startBeat": 48, "endBeat": 60, "intensity": 0.82}, + ]} + return stereo, analysis, sections + + +def build(output: Path = ROOT / "web" / "assets" / "demo") -> dict[str, Any]: + output = output.resolve() + output.mkdir(parents=True, exist_ok=True) + # All temporary map files live beneath the selected output, never in a game/cache folder. + with tempfile.TemporaryDirectory(prefix="demo-build-", dir=output) as temporary: + folder = Path(temporary) + reports = folder / "_beatforge" + reports.mkdir() + audio, analysis, sections = synthesize_score() + wav_path = folder / "source.wav" + with wave.open(str(wav_path), "wb") as wav: + wav.setnchannels(2) + wav.setsampwidth(2) + wav.setframerate(SAMPLE_RATE) + wav.writeframes((np.clip(audio, -1, 1) * 32767).astype("', '\n ') + .replaceAll('src="/web/', 'src="').replaceAll('href="/web/', 'href="') + .replaceAll('"/assets/', '"assets/').replaceAll("'/assets/", "'assets/"); +for (const name of await readdir(source)) { + if (!['.js', '.css'].includes(path.extname(name))) continue; + const file = path.join(source, name); + await copyFile(file, path.join(output, name)); + const revision = createHash('sha256').update(await readFile(file)).digest('hex').slice(0, 12); + html = html.replaceAll(`"${name}"`, `"${name}?v=${revision}"`); +} +await writeFile(path.join(output, 'index.html'), html); +await cp(path.join(source, 'assets'), path.join(output, 'assets'), {recursive: true}); diff --git a/tools/build_static_demo.py b/tools/build_static_demo.py index e6f8640..012375f 100644 --- a/tools/build_static_demo.py +++ b/tools/build_static_demo.py @@ -3,6 +3,7 @@ from __future__ import annotations import shutil +import hashlib from pathlib import Path @@ -14,15 +15,19 @@ def build() -> None: OUTPUT.mkdir(parents=True, exist_ok=True) html = (SOURCE / "index.html").read_text(encoding="utf-8") - html = html.replace('src="/web/webmcp.js"', 'src="webmcp.js"') + html = html.replace('', '\n ') + html = html.replace('src="/web/', 'src="') + html = html.replace('href="/web/', 'href="') html = html.replace('"/assets/', '"assets/') html = html.replace("'/assets/", "'assets/") - (OUTPUT / "index.html").write_text(html, encoding="utf-8") - shutil.copy2(SOURCE / "webmcp.js", OUTPUT / "webmcp.js") + for file in SOURCE.iterdir(): + if file.suffix in {'.js', '.css'}: + shutil.copy2(file, OUTPUT / file.name) + revision = hashlib.sha256(file.read_bytes()).hexdigest()[:12] + html = html.replace(f'"{file.name}"', f'"{file.name}?v={revision}"') + (OUTPUT / "index.html").write_text(html, encoding="utf-8", newline="\n") assets = OUTPUT / "assets" - if assets.exists(): - shutil.rmtree(assets) - shutil.copytree(SOURCE / "assets", assets) + shutil.copytree(SOURCE / "assets", assets, dirs_exist_ok=True) if __name__ == "__main__": diff --git a/tools/check_connector.py b/tools/check_connector.py new file mode 100644 index 0000000..01b449a --- /dev/null +++ b/tools/check_connector.py @@ -0,0 +1,86 @@ +"""Read-only HTTP check using the official MCP client and the same REST account.""" +import argparse +import asyncio +import hashlib +import json +import os +import re +from urllib.parse import urlsplit + +import httpx +from mcp import ClientSession +from mcp.client.streamable_http import streamable_http_client + + +async def check(gateway, token, job_id=None): + parsed = urlsplit(gateway) + if (parsed.username or parsed.password or parsed.query or parsed.fragment or + parsed.path not in {'', '/'} or not parsed.hostname or + (parsed.scheme != 'https' and not (parsed.scheme == 'http' and parsed.hostname in {'localhost', '127.0.0.1', '::1'}))): + raise ValueError('Expected HTTPS origin or local HTTP origin') + if job_id and not re.fullmatch('[a-f0-9]{32}', job_id): + raise ValueError('Invalid job identifier') + gateway = gateway.rstrip('/') + async with httpx.AsyncClient(headers={'Authorization': f'Bearer {token}'}, timeout=30, + follow_redirects=False) as http: + async def rest(path, body=None): + reply = await (http.get(gateway + path) if body is None else http.post(gateway + path, json=body)) + reply.raise_for_status() + return reply.json() + async with streamable_http_client(gateway + '/mcp', http_client=http) as (read, write, _): + async with ClientSession(read, write) as session: + initialized = await session.initialize() + assert initialized.serverInfo.name == 'BeatForge' + listed = await session.list_tools() + names = {tool.name for tool in listed.tools} + assert {'get_capabilities', 'resolve_mapping_plan', 'get_job', 'get_qa', 'get_artifacts'} <= names + async def call(name, arguments=None): + result = await session.call_tool(name, arguments or {}) + if result.isError: + raise RuntimeError(f'MCP operation failed: {name}') + return json.loads(next(item.text for item in result.content if item.type == 'text')) + plan = {'brief': 'lighter verses, no bombs', 'style': 'flow'} + assert await call('resolve_mapping_plan', {'plan': plan}) == await rest('/api/mapping-plan/resolve', plan) + report = {'transport': 'real HTTP', 'client': 'official Python MCP SDK', + 'server': initialized.serverInfo.name, 'tools': len(names), 'planParity': True, + 'assistantClientsVerified': False, 'downloadsVerified': 0} + if job_id: + job = await rest('/api/jobs/' + job_id) + assert await call('get_job', {'job_id': job_id}) == job + report['jobParity'] = True + if job['result'] and job['result']['status'] == 'playtest_candidate': + qa = await rest(f'/api/jobs/{job_id}/qa') + assert await call('get_qa', {'job_id': job_id}) == qa + report['qaParity'] = True + manifest = await call('get_artifacts', {'job_id': job_id}) + for item in manifest['artifacts']: + assert re.fullmatch('[a-f0-9]{32}', item['id']) + digest, size = hashlib.sha256(), 0 + async with http.stream('GET', f"{gateway}/api/jobs/{job_id}/artifacts/{item['id']}") as response: + response.raise_for_status() + async for chunk in response.aiter_bytes(): + size += len(chunk) + if size > 64 * 1024 * 1024: + raise ValueError('Artifact exceeded bounded check size') + digest.update(chunk) + assert size == item['size'] and digest.hexdigest() == item['sha256'] + report['downloadsVerified'] += 1 + return report + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--gateway', required=True) + parser.add_argument('--job') + args = parser.parse_args() + try: + report = asyncio.run(check(args.gateway, os.environ['BEATFORGE_CHECK_TOKEN'], args.job)) + except Exception as error: + # Do not print exception URLs, signed tickets or credentials. + print(json.dumps({'ok': False, 'errorType': type(error).__name__})) + raise SystemExit(1) + print(json.dumps({'ok': True, **report}, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/tools/doctor.py b/tools/doctor.py new file mode 100644 index 0000000..2f8752e --- /dev/null +++ b/tools/doctor.py @@ -0,0 +1,59 @@ +"""Read-only dependency checks for the local Studio. Does not download models.""" +from __future__ import annotations + +import argparse +import importlib +import importlib.util +import json +import subprocess +import sys +from pathlib import Path + + +def inspect() -> dict: + checks = [] + for module in ("beatforge", "fastapi", "uvicorn", "numpy", "soundfile", "ortools.sat.python.cp_model", "UnityPy"): + try: + importlib.import_module(module) + checks.append({"name": module, "ok": True, "required": True}) + except Exception as error: + checks.append({"name": module, "ok": False, "required": True, "detail": str(error), "action": 'Run Start-BeatForge.ps1 -Setup or python -m pip install -e ".[dev,studio]".'}) + try: + import imageio_ffmpeg + result = subprocess.run([imageio_ffmpeg.get_ffmpeg_exe(), "-version"], capture_output=True, text=True, timeout=15) + if result.returncode: + raise RuntimeError(result.stderr[:300]) + checks.append({"name": "FFmpeg", "ok": True, "required": True, "detail": result.stdout.splitlines()[0]}) + except Exception as error: + checks.append({"name": "FFmpeg", "ok": False, "required": True, "detail": str(error), "action": "Install imageio-ffmpeg into this environment."}) + for module in ("torch", "beat_this", "demucs"): + available = importlib.util.find_spec(module) is not None + checks.append({"name": module, "ok": available, "required": False, "detail": "Package found; model weights and runtime have not been tested." if available else "Optional package missing. RL requires Torch; uncertain analysis requests timing anchors."}) + try: + from beatforge.premium import corpus_database + available = corpus_database().is_file() + checks.append({"name": "Official corpus", "ok": available, "required": False, "detail": "Database found; completeness is checked during generation." if available else "Full premium generation needs the local official corpus. The bundled sample works without it."}) + except Exception as error: + checks.append({"name": "Official corpus", "ok": False, "required": False, "detail": str(error)}) + return {"python": sys.executable, "studioReady": all(row["ok"] for row in checks if row["required"]), "checks": checks} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--json", action="store_true") + args = parser.parse_args() + report = inspect() + if args.json: + print(json.dumps(report, indent=2)) + else: + print(f"Python: {report['python']}") + for item in report["checks"]: + label = "OK" if item["ok"] else "MISSING" if item["required"] else "OPTIONAL" + print(f"[{label}] {item['name']}: {item.get('detail', '')}") + if item.get("action"): + print(f" {item['action']}") + return 0 if report["studioReady"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/smoke_connector.py b/tools/smoke_connector.py new file mode 100644 index 0000000..713689f --- /dev/null +++ b/tools/smoke_connector.py @@ -0,0 +1,68 @@ +"""Exercise real local generation through the connector with repository synthetic audio.""" +import json +import os +import sqlite3 +import secrets +import time +import uuid +from pathlib import Path + +from fastapi.testclient import TestClient + +from beatforge.connector import Principal, create_app +from beatforge.connector_worker import run_once +from beatforge.premium import corpus_database + +ROOT = Path(__file__).resolve().parents[1] + + +def main(): + output = ROOT/'data/connector-smoke'/uuid.uuid4().hex + output.mkdir(parents=True) + source = corpus_database() + target = output/'official-corpus.sqlite3' + if source.is_file(): + with sqlite3.connect(source.resolve().as_uri()+'?mode=ro', uri=True) as original, sqlite3.connect(target) as copy: + original.backup(copy) + os.environ['BEATFORGE_CORPUS_DB'] = str(target) + account_token, worker_token = secrets.token_urlsafe(32), secrets.token_urlsafe(32) + app = create_app(database=output/'jobs.db', hosts=['testserver'], credentials={ + account_token: Principal('synthetic-smoke', frozenset({'read', 'generate', 'edit'})), + worker_token: Principal('synthetic-smoke', frozenset({'worker'}))}) + started = time.monotonic() + with TestClient(app) as client: + client.headers['Authorization'] = 'Bearer '+account_token + audio = (ROOT/'web/assets/demo/song.ogg').read_bytes() + response = client.post('/api/uploads', json={'filename': 'synthetic.ogg', 'size': len(audio)}) + response.raise_for_status() + upload = response.json() + client.put(upload['uploadPath'], content=audio).raise_for_status() + response = client.post('/api/jobs', json={'uploadId': upload['id'], 'title': 'Connector synthetic smoke', + 'artist': 'BeatForge synthetic score', 'idempotencyKey': 'smoke', 'difficulties': ['Hard'], + 'mappingPlan': {'brief': 'flow, lighter verses, no bombs'}, 'allowUnconfirmed': True}) + response.raise_for_status() + job_id = response.json()['id'] + print(f'Running real synthetic generation: {job_id}', flush=True) + client.headers['Authorization'] = 'Bearer '+worker_token + run_once(client, output/'worker') + client.headers['Authorization'] = 'Bearer '+account_token + job = client.get('/api/jobs/'+job_id).json() + artifacts = client.get(f'/api/jobs/{job_id}/artifacts').json()['artifacts'] + downloads = [] + for item in artifacts: + download = client.get(item['downloadUrl'], headers={'Authorization': ''}) + download.raise_for_status() + assert len(download.content) == item['size'] + downloads.append(item['name']) + report = {'jobId': job_id, 'state': job['state'], 'result': job['result'], 'downloadsVerified': downloads, + 'elapsedSeconds': round(time.monotonic()-started, 2), 'transport': 'in-process ASGI', + 'liveClientVerified': False, 'humanPlaytestVerified': False} + (output/'report.json').write_text(json.dumps(report, indent=2), encoding='utf-8') + print(json.dumps({'report': str(output/'report.json'), 'status': job['result']['status'], + 'downloads': downloads, 'seconds': report['elapsedSeconds']}), flush=True) + if job['result']['status'] != 'playtest_candidate': + raise SystemExit(1) + + +if __name__ == '__main__': + main() diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..ed90b0c --- /dev/null +++ b/vercel.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": null, + "installCommand": "node --version", + "buildCommand": "node tools/build_static_demo.mjs", + "outputDirectory": "site", + "headers": [ + { + "source": "/(.*)", + "headers": [ + {"key": "X-Content-Type-Options", "value": "nosniff"}, + {"key": "Referrer-Policy", "value": "no-referrer"}, + {"key": "X-Frame-Options", "value": "DENY"} + ] + } + ] +} diff --git a/web/assets/beat-forge-logo.png b/web/assets/beat-forge-logo.png index 08911f6..d48ef0f 100644 Binary files a/web/assets/beat-forge-logo.png and b/web/assets/beat-forge-logo.png differ diff --git a/web/assets/demo/map.zip b/web/assets/demo/map.zip new file mode 100644 index 0000000..f5c0ec6 Binary files /dev/null and b/web/assets/demo/map.zip differ diff --git a/web/assets/demo/preview.json b/web/assets/demo/preview.json new file mode 100644 index 0000000..dcdafff --- /dev/null +++ b/web/assets/demo/preview.json @@ -0,0 +1 @@ +{"schemaVersion":1,"title":"Neon Rain","artist":"BeatForge synthetic score","difficulty":"Hard","difficulties":["Easy","Normal","Hard","Expert","ExpertPlus"],"filename":"HardStandard.dat","bpm":120.0,"offsetSeconds":0.0,"duration":30.0,"waveform":[0.4268,0.2477,0.1461,0.0845,0.0476,0.0297,0.0178,0.0094,0.0061,0.0001,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.2035,0.7185,0.349,0.1946,0.0987,0.0607,0.0381,0.0178,0.0094,0.006,0.0001,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.2509,0.4254,0.2472,0.1369,0.0789,0.0484,0.0276,0.0178,0.0093,0.0061,0.0001,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.705,0.7394,0.3509,0.1958,0.1114,0.0577,0.0323,0.0177,0.0094,0.0062,0.0001,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.4209,0.5069,0.3077,0.1801,0.1418,0.031,0.0327,0.0338,0.031,0.0254,0.014,0.0114,0.0085,0.0063,0.0001,0.0,0.0,0.0,0.0,0.0,0.7185,0.7933,0.399,0.2236,0.1564,0.0401,0.0331,0.0335,0.0312,0.0255,0.0141,0.0114,0.0087,0.0063,0.0001,0.0,0.0,0.0,0.0,0.0,0.4521,0.5074,0.3129,0.1819,0.1311,0.031,0.0323,0.0334,0.0308,0.0257,0.0141,0.0112,0.0086,0.0063,0.0001,0.0,0.0,0.0,0.0,0.0,0.7269,0.7582,0.3937,0.23,0.1542,0.0386,0.0361,0.0331,0.031,0.0256,0.0139,0.0109,0.0086,0.0063,0.0,0.0,0.0,0.0,0.0,0.0,0.4882,0.4872,0.3784,0.1786,0.0543,0.0752,0.0659,0.0344,0.0285,0.0252,0.0584,0.0524,0.0118,0.0065,0.0,0.0,0.0,0.0,0.0,0.0,0.7225,0.711,0.4412,0.2158,0.092,0.0852,0.0658,0.031,0.0283,0.0249,0.0608,0.0342,0.0119,0.0066,0.0001,0.0,0.0,0.0,0.0,0.0,0.4734,0.4709,0.371,0.1784,0.0553,0.0761,0.0656,0.0278,0.0283,0.0245,0.0937,0.0399,0.0115,0.0065,0.0,0.0,0.0,0.0,0.0,0.0,0.672,0.6952,0.4289,0.2237,0.0847,0.0838,0.0678,0.0249,0.0285,0.0239,0.0455,0.046,0.0127,0.0064,0.0001,0.0,0.0,0.0,0.0,0.0,0.4952,0.4735,0.3351,0.179,0.0529,0.0752,0.0664,0.0245,0.0283,0.023,0.0523,0.0459,0.0111,0.0065,0.0,0.0,0.0,0.0,0.0,0.0,0.6589,0.6997,0.3413,0.2196,0.0762,0.0855,0.0677,0.0245,0.0282,0.0222,0.0468,0.0339,0.0122,0.0065,0.0001,0.0,0.0,0.0,0.0,0.0,0.4886,0.4761,0.2701,0.1787,0.0493,0.0755,0.0661,0.0242,0.028,0.0211,0.0454,0.0338,0.0119,0.0062,0.0,0.0,0.0,0.0,0.0,0.0,0.6673,0.67,0.3265,0.2138,0.069,0.0871,0.0671,0.0247,0.028,0.0212,0.0603,0.041,0.0113,0.0061,0.0001,0.0,0.0,0.0,0.0,0.0,0.5034,0.5875,0.3466,0.1996,0.1231,0.1147,0.0512,0.0335,0.0276,0.0202,0.053,0.0355,0.0101,0.0062,0.0,0.0,0.0,0.0,0.0,0.0,0.7802,0.7084,0.3853,0.2549,0.1537,0.114,0.0476,0.0341,0.0256,0.0204,0.0607,0.031,0.0105,0.0062,0.0001,0.0,0.0,0.0,0.0,0.0,0.4846,0.595,0.3493,0.2367,0.1226,0.1107,0.0518,0.0375,0.0282,0.0204,0.051,0.0397,0.0109,0.0062,0.0,0.0,0.0,0.0,0.0,0.0,0.7216,0.7706,0.4015,0.2459,0.148,0.1127,0.0475,0.0342,0.0258,0.0203,0.069,0.0322,0.0108,0.0062,0.0001,0.0,0.0,0.0,0.0,0.0,0.5203,0.5932,0.3442,0.2007,0.1222,0.1132,0.0513,0.0338,0.0277,0.0204,0.0578,0.0306,0.0101,0.0061,0.0,0.0,0.0,0.0,0.0,0.0,0.6632,0.7656,0.4241,0.2557,0.1375,0.1096,0.0474,0.0334,0.0259,0.0201,0.0666,0.0345,0.0098,0.0062,0.0,0.0,0.0,0.0,0.0,0.0,0.4783,0.595,0.353,0.2366,0.1231,0.1099,0.0522,0.0376,0.0281,0.0203,0.0588,0.0257,0.0109,0.0063,0.0,0.0,0.0,0.0,0.0,0.0,0.6901,0.8104,0.3931,0.239,0.1539,0.1115,0.0478,0.0338,0.0256,0.0201,0.066,0.0297,0.0103,0.0061,0.0,0.0,0.0,0.0,0.0,0.0,0.5348,0.5881,0.2706,0.1888,0.1577,0.1107,0.067,0.0353,0.0193,0.0178,0.105,0.0839,0.0455,0.0292,0.0159,0.0103,0.0065,0.004,0.0026,0.0001,0.6674,0.6537,0.4085,0.2108,0.1823,0.1106,0.0745,0.0366,0.0196,0.0177,0.128,0.0854,0.0455,0.0302,0.0159,0.0101,0.0063,0.0041,0.0026,0.0001,0.574,0.5396,0.3089,0.1889,0.1592,0.103,0.0703,0.0372,0.0198,0.0178,0.1172,0.0787,0.0435,0.0293,0.0158,0.0101,0.0065,0.004,0.0026,0.0001,0.7054,0.6803,0.3949,0.1923,0.1661,0.108,0.0741,0.0352,0.019,0.0179,0.1103,0.0869,0.0422,0.0307,0.0158,0.0101,0.0064,0.0041,0.0026,0.0,0.5743,0.4784,0.2718,0.1869,0.1586,0.1061,0.0663,0.035,0.0195,0.0177,0.1469,0.092,0.0423,0.0293,0.0156,0.0101,0.0062,0.004,0.0026,0.0001,0.8413,0.6552,0.419,0.2139,0.1711,0.1169,0.0765,0.0363,0.0199,0.0179,0.1241,0.0858,0.042,0.0291,0.0154,0.0099,0.0063,0.004,0.0026,0.0001,0.5672,0.488,0.3082,0.1899,0.144,0.0969,0.0704,0.0376,0.0198,0.0179,0.1171,0.0881,0.0424,0.0293,0.0155,0.0097,0.0063,0.004,0.0026,0.0001,0.7832,0.6975,0.429,0.1969,0.1698,0.102,0.0749,0.0349,0.0189,0.0174,0.1112,0.0833,0.0421,0.0254,0.0155,0.0099,0.0063,0.004,0.0026,0.0,0.5995,0.4764,0.3107,0.202,0.0747,0.0379,0.0327,0.0334,0.0298,0.022,0.1095,0.0776,0.0426,0.0233,0.0155,0.0097,0.0063,0.0039,0.0026,0.0001,0.7125,0.6397,0.4015,0.2438,0.1046,0.0442,0.0369,0.0368,0.0334,0.0218,0.1118,0.085,0.0425,0.0231,0.0153,0.01,0.0063,0.0038,0.0025,0.0001,0.5898,0.4789,0.3209,0.194,0.089,0.0374,0.0365,0.0368,0.0326,0.0218,0.1069,0.09,0.0429,0.0233,0.015,0.0096,0.0063,0.004,0.0024,0.0001,0.821,0.6885,0.4443,0.2439,0.1122,0.0493,0.0307,0.0337,0.0333,0.0219,0.109,0.0783,0.0431,0.0251,0.0152,0.0097,0.0061,0.0039,0.0025,0.0001,0.5989,0.4731,0.3255,0.199,0.0753,0.0378,0.0327,0.0323,0.03,0.0219,0.1066,0.083,0.0425,0.0235,0.015,0.0096,0.0061,0.004,0.0025,0.0001,0.7131,0.6571,0.4411,0.2522,0.1021,0.0447,0.0376,0.0364,0.0332,0.0222,0.1228,0.0801,0.0429,0.0229,0.0148,0.0094,0.0061,0.0039,0.0024,0.0001,0.5899,0.4755,0.3146,0.195,0.0892,0.0381,0.0369,0.0367,0.0323,0.022,0.1119,0.0814,0.0427,0.0234,0.015,0.0094,0.0061,0.0038,0.0025,0.0001,0.7422,0.7142,0.4122,0.2559,0.0913,0.0463,0.0309,0.0337,0.0328,0.0217,0.1014,0.0772,0.0431,0.0245,0.0148,0.0096,0.0061,0.0039,0.0025,0.0,0.5425,0.4615,0.2848,0.1345,0.0681,0.0773,0.0678,0.0205,0.028,0.0213,0.0594,0.0194,0.008,0.0059,0.0,0.0,0.0,0.0,0.0,0.0,0.7309,0.5943,0.3528,0.1788,0.0847,0.0874,0.0668,0.0198,0.0288,0.0217,0.0502,0.025,0.0081,0.0057,0.0,0.0,0.0,0.0,0.0,0.0,0.5369,0.4235,0.2801,0.1647,0.0686,0.0801,0.0713,0.0231,0.0297,0.0213,0.053,0.0225,0.008,0.0055,0.0,0.0,0.0,0.0,0.0,0.0,0.6806,0.6536,0.316,0.1945,0.0781,0.0827,0.065,0.0232,0.0281,0.0211,0.0509,0.0235,0.0081,0.0055,0.0,0.0,0.0,0.0,0.0,0.0,0.5311,0.463,0.2891,0.1323,0.072,0.0776,0.0671,0.023,0.0284,0.0211,0.0608,0.021,0.0078,0.0002,0.0,0.0,0.0,0.0,0.0,0.0,0.6948,0.5716,0.3304,0.1823,0.0865,0.0857,0.0592,0.0265,0.0285,0.0214,0.0425,0.0214,0.0079,0.0003,0.0,0.0,0.0,0.0,0.0,0.0,0.5331,0.4266,0.2881,0.1649,0.0675,0.0803,0.0611,0.0254,0.0295,0.0216,0.0515,0.0229,0.0081,0.0002,0.0,0.0,0.0,0.0,0.0,0.0,0.8589,0.6489,0.3408,0.202,0.0836,0.0827,0.0571,0.0282,0.0285,0.0215,0.0445,0.0223,0.0079,0.0005,0.0,0.0,0.0,0.0,0.0,0.0,0.6202,0.4445,0.312,0.1243,0.1227,0.0781,0.0507,0.0337,0.028,0.0162,0.1094,0.0741,0.0412,0.0225,0.014,0.0091,0.0058,0.0037,0.0024,0.0001,0.7722,0.5953,0.3707,0.1965,0.1448,0.0879,0.0522,0.0374,0.028,0.016,0.1183,0.0654,0.0415,0.0224,0.0143,0.0092,0.0058,0.0037,0.0024,0.0,0.5991,0.4531,0.2419,0.1546,0.1227,0.079,0.0517,0.0373,0.0281,0.0161,0.1119,0.0893,0.0411,0.0222,0.0142,0.0092,0.0057,0.0036,0.0023,0.0,0.7391,0.6435,0.3203,0.1947,0.1409,0.0873,0.0517,0.0346,0.0261,0.0162,0.1295,0.0665,0.0416,0.0224,0.0144,0.0092,0.0058,0.0037,0.0023,0.0,0.6026,0.4373,0.1981,0.1242,0.1238,0.0784,0.0512,0.0332,0.0278,0.016,0.1153,0.0673,0.0411,0.0218,0.014,0.0089,0.0058,0.0036,0.0023,0.0,0.7794,0.5984,0.2599,0.1651,0.136,0.0852,0.0523,0.0376,0.0269,0.0162,0.1057,0.0709,0.041,0.0216,0.0143,0.0087,0.0057,0.0036,0.0023,0.0,0.589,0.4539,0.2384,0.1423,0.1218,0.0805,0.0516,0.0373,0.0259,0.0161,0.1048,0.0639,0.0412,0.0218,0.0141,0.009,0.0056,0.0036,0.0024,0.0,0.8124,0.6168,0.2529,0.1637,0.1449,0.0893,0.052,0.0349,0.0264,0.0162,0.1209,0.0713,0.0415,0.022,0.014,0.0089,0.0057,0.0036,0.0023,0.0001,0.5645,0.4899,0.2693,0.1874,0.1327,0.0866,0.0602,0.0304,0.0175,0.0161,0.1065,0.0639,0.0421,0.0218,0.0138,0.009,0.0055,0.0035,0.0023,0.0,0.7301,0.6396,0.3924,0.2085,0.1617,0.0916,0.0626,0.031,0.0179,0.016,0.1005,0.0599,0.0357,0.0214,0.0137,0.0088,0.0056,0.0036,0.0023,0.0001,0.512,0.4882,0.2842,0.1726,0.1348,0.083,0.0551,0.031,0.018,0.0161,0.0502,0.0159,0.008,0.0003,0.0,0.0,0.0,0.0,0.0,0.0,0.7362,0.6096,0.3876,0.2095,0.1531,0.0914,0.057,0.0305,0.0171,0.0151,0.0484,0.0127,0.0054,0.0001,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"chart":{"version":"3.3.0","bpmEvents":[],"rotationEvents":[],"colorNotes":[{"b":4.0,"x":3,"y":1,"c":1,"d":7,"a":0},{"b":4.5,"x":1,"y":0,"c":0,"d":7,"a":0},{"b":5.0,"x":3,"y":0,"c":1,"d":0,"a":0},{"b":8.0,"x":1,"y":0,"c":0,"d":2,"a":0},{"b":8.5,"x":0,"y":0,"c":0,"d":3,"a":0},{"b":12.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":12.5,"x":2,"y":0,"c":1,"d":0,"a":0},{"b":16.0,"x":1,"y":0,"c":0,"d":0,"a":0},{"b":16.5,"x":2,"y":1,"c":0,"d":1,"a":0},{"b":20.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":20.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":24.0,"x":0,"y":0,"c":0,"d":3,"a":0},{"b":24.5,"x":1,"y":0,"c":0,"d":2,"a":0},{"b":25.0,"x":3,"y":1,"c":1,"d":3,"a":0},{"b":27.0,"x":3,"y":1,"c":1,"d":2,"a":0},{"b":27.5,"x":2,"y":1,"c":1,"d":3,"a":0},{"b":28.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":29.0,"x":2,"y":1,"c":1,"d":0,"a":0},{"b":29.5,"x":1,"y":2,"c":1,"d":1,"a":0},{"b":31.0,"x":1,"y":1,"c":0,"d":1,"a":0},{"b":31.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":32.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":33.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":33.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":35.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":35.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":36.0,"x":0,"y":1,"c":0,"d":1,"a":0},{"b":37.0,"x":2,"y":1,"c":1,"d":1,"a":0},{"b":37.5,"x":1,"y":0,"c":1,"d":0,"a":0},{"b":39.0,"x":1,"y":1,"c":0,"d":0,"a":0},{"b":39.5,"x":2,"y":2,"c":0,"d":1,"a":0},{"b":44.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":44.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":48.0,"x":0,"y":0,"c":0,"d":3,"a":0},{"b":48.5,"x":1,"y":0,"c":0,"d":2,"a":0},{"b":49.0,"x":3,"y":1,"c":1,"d":3,"a":0},{"b":51.0,"x":1,"y":1,"c":0,"d":2,"a":0},{"b":51.5,"x":0,"y":1,"c":0,"d":3,"a":0},{"b":52.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":53.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":53.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":55.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":55.5,"x":2,"y":0,"c":1,"d":0,"a":0},{"b":56.0,"x":0,"y":1,"c":0,"d":1,"a":0},{"b":57.0,"x":2,"y":1,"c":1,"d":0,"a":0},{"b":57.5,"x":1,"y":2,"c":1,"d":1,"a":0},{"b":59.0,"x":1,"y":1,"c":0,"d":0,"a":0},{"b":59.5,"x":2,"y":2,"c":0,"d":1,"a":0}],"bombNotes":[{"b":5.0,"x":0,"y":2},{"b":30.0,"x":3,"y":2}],"obstacles":[{"b":44.5,"d":1.0,"x":0,"y":0,"w":2,"h":5}],"sliders":[{"c":1,"b":4.0,"x":3,"y":1,"d":7,"mu":0.7,"tb":5.0,"tx":3,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":8.0,"x":1,"y":0,"d":2,"mu":0.7,"tb":8.5,"tx":0,"ty":0,"tc":3,"tmu":0.7,"m":0},{"c":1,"b":12.0,"x":3,"y":1,"d":1,"mu":0.7,"tb":12.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":16.0,"x":1,"y":0,"d":0,"mu":0.7,"tb":16.5,"tx":2,"ty":1,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":20.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":20.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":24.0,"x":0,"y":0,"d":3,"mu":0.7,"tb":24.5,"tx":1,"ty":0,"tc":2,"tmu":0.7,"m":0},{"c":1,"b":27.0,"x":3,"y":1,"d":2,"mu":0.7,"tb":27.5,"tx":2,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":1,"b":29.0,"x":2,"y":1,"d":0,"mu":0.7,"tb":29.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":31.0,"x":1,"y":1,"d":1,"mu":0.7,"tb":31.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":33.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":33.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":35.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":35.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":37.0,"x":2,"y":1,"d":1,"mu":0.7,"tb":37.5,"tx":1,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":39.0,"x":1,"y":1,"d":0,"mu":0.7,"tb":39.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":44.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":44.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":48.0,"x":0,"y":0,"d":3,"mu":0.7,"tb":48.5,"tx":1,"ty":0,"tc":2,"tmu":0.7,"m":0},{"c":0,"b":51.0,"x":1,"y":1,"d":2,"mu":0.7,"tb":51.5,"tx":0,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":0,"b":53.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":53.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":55.0,"x":3,"y":1,"d":1,"mu":0.7,"tb":55.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":57.0,"x":2,"y":1,"d":0,"mu":0.7,"tb":57.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":59.0,"x":1,"y":1,"d":0,"mu":0.7,"tb":59.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0}],"burstSliders":[],"waypoints":[],"basicBeatmapEvents":[{"b":0.0,"et":0,"i":5,"f":0.45},{"b":0.0,"et":1,"i":7,"f":0.35},{"b":4.0,"et":0,"i":1,"f":0.45},{"b":4.0,"et":1,"i":6,"f":1.0},{"b":8.0,"et":0,"i":5,"f":0.65},{"b":8.0,"et":1,"i":2,"f":1.0},{"b":8.0,"et":4,"i":5,"f":0.4},{"b":8.5,"et":2,"i":7,"f":0.32},{"b":9.0,"et":2,"i":7,"f":0.32},{"b":9.5,"et":2,"i":7,"f":0.32},{"b":10.0,"et":1,"i":7,"f":0.4},{"b":10.0,"et":4,"i":5,"f":0.4},{"b":10.5,"et":2,"i":7,"f":0.32},{"b":11.0,"et":2,"i":7,"f":0.32},{"b":11.5,"et":2,"i":7,"f":0.32},{"b":12.0,"et":0,"i":1,"f":0.65},{"b":12.0,"et":1,"i":3,"f":0.55},{"b":12.0,"et":4,"i":1,"f":0.4},{"b":12.5,"et":2,"i":3,"f":0.32},{"b":13.0,"et":2,"i":3,"f":0.32},{"b":13.5,"et":2,"i":3,"f":0.32},{"b":14.0,"et":1,"i":3,"f":0.4},{"b":14.0,"et":4,"i":1,"f":0.4},{"b":14.5,"et":2,"i":3,"f":0.32},{"b":15.0,"et":2,"i":3,"f":0.32},{"b":15.5,"et":2,"i":3,"f":0.32},{"b":16.0,"et":0,"i":5,"f":0.65},{"b":16.0,"et":1,"i":7,"f":0.55},{"b":16.0,"et":4,"i":5,"f":0.4},{"b":16.5,"et":2,"i":7,"f":0.32},{"b":17.0,"et":2,"i":7,"f":0.32},{"b":17.5,"et":2,"i":7,"f":0.32},{"b":18.0,"et":1,"i":7,"f":0.4},{"b":18.0,"et":4,"i":5,"f":0.4},{"b":18.5,"et":2,"i":7,"f":0.32},{"b":19.0,"et":2,"i":7,"f":0.32},{"b":19.5,"et":2,"i":7,"f":0.32},{"b":20.0,"et":0,"i":1,"f":0.65},{"b":20.0,"et":1,"i":3,"f":0.55},{"b":20.0,"et":4,"i":1,"f":0.4},{"b":20.5,"et":2,"i":3,"f":0.32},{"b":21.0,"et":2,"i":3,"f":0.32},{"b":21.5,"et":2,"i":3,"f":0.32},{"b":22.0,"et":1,"i":3,"f":0.4},{"b":22.0,"et":4,"i":1,"f":0.4},{"b":22.5,"et":2,"i":3,"f":0.32},{"b":23.0,"et":2,"i":3,"f":0.32},{"b":23.5,"et":2,"i":3,"f":0.32},{"b":24.0,"et":0,"i":5,"f":0.95},{"b":24.0,"et":1,"i":6,"f":1.0},{"b":24.0,"et":2,"i":6,"f":0.85},{"b":24.0,"et":3,"i":6,"f":0.85},{"b":24.0,"et":4,"i":6,"f":0.95},{"b":24.24997732,"et":2,"i":7,"f":0.35},{"b":24.5,"et":2,"i":6,"f":0.55},{"b":24.5,"et":4,"i":7,"f":0.45},{"b":24.75002268,"et":2,"i":7,"f":0.35},{"b":25.0,"et":2,"i":6,"f":0.55},{"b":25.0,"et":4,"i":7,"f":0.45},{"b":25.24997732,"et":2,"i":7,"f":0.35},{"b":25.5,"et":2,"i":6,"f":0.55},{"b":25.5,"et":4,"i":7,"f":0.45},{"b":25.75002268,"et":2,"i":7,"f":0.35},{"b":26.0,"et":1,"i":7,"f":0.75},{"b":26.0,"et":2,"i":5,"f":0.55},{"b":26.0,"et":3,"i":5,"f":0.55},{"b":26.0,"et":4,"i":5,"f":0.6},{"b":26.24997732,"et":2,"i":7,"f":0.35},{"b":26.5,"et":2,"i":6,"f":0.55},{"b":26.5,"et":4,"i":7,"f":0.45},{"b":26.75002268,"et":2,"i":7,"f":0.35},{"b":27.0,"et":2,"i":6,"f":0.55},{"b":27.0,"et":4,"i":7,"f":0.45},{"b":27.24997732,"et":2,"i":7,"f":0.35},{"b":27.5,"et":2,"i":6,"f":0.55},{"b":27.5,"et":4,"i":7,"f":0.45},{"b":27.75002268,"et":2,"i":7,"f":0.35},{"b":28.0,"et":0,"i":1,"f":0.95},{"b":28.0,"et":1,"i":2,"f":1.0},{"b":28.0,"et":2,"i":2,"f":0.85},{"b":28.0,"et":3,"i":2,"f":0.85},{"b":28.0,"et":4,"i":2,"f":0.95},{"b":28.24997732,"et":2,"i":3,"f":0.35},{"b":28.5,"et":2,"i":2,"f":0.55},{"b":28.5,"et":4,"i":3,"f":0.45},{"b":28.75002268,"et":2,"i":3,"f":0.35},{"b":29.0,"et":2,"i":2,"f":0.55},{"b":29.0,"et":4,"i":3,"f":0.45},{"b":29.24997732,"et":2,"i":3,"f":0.35},{"b":29.5,"et":2,"i":2,"f":0.55},{"b":29.5,"et":4,"i":3,"f":0.45},{"b":29.75002268,"et":2,"i":3,"f":0.35},{"b":30.0,"et":1,"i":3,"f":0.75},{"b":30.0,"et":2,"i":1,"f":0.55},{"b":30.0,"et":3,"i":1,"f":0.55},{"b":30.0,"et":4,"i":1,"f":0.6},{"b":30.24997732,"et":2,"i":3,"f":0.35},{"b":30.5,"et":2,"i":2,"f":0.55},{"b":30.5,"et":4,"i":3,"f":0.45},{"b":30.75002268,"et":2,"i":3,"f":0.35},{"b":31.0,"et":2,"i":2,"f":0.55},{"b":31.0,"et":4,"i":3,"f":0.45},{"b":31.24997732,"et":2,"i":3,"f":0.35},{"b":31.5,"et":2,"i":2,"f":0.55},{"b":31.5,"et":4,"i":3,"f":0.45},{"b":31.75002268,"et":2,"i":3,"f":0.35},{"b":32.0,"et":0,"i":5,"f":0.95},{"b":32.0,"et":1,"i":6,"f":1.0},{"b":32.0,"et":2,"i":6,"f":0.85},{"b":32.0,"et":3,"i":6,"f":0.85},{"b":32.0,"et":4,"i":6,"f":0.95},{"b":32.24997732,"et":2,"i":7,"f":0.35},{"b":32.5,"et":2,"i":6,"f":0.55},{"b":32.5,"et":4,"i":7,"f":0.45},{"b":32.75002268,"et":2,"i":7,"f":0.35},{"b":33.0,"et":2,"i":6,"f":0.55},{"b":33.0,"et":4,"i":7,"f":0.45},{"b":33.24997732,"et":2,"i":7,"f":0.35},{"b":33.5,"et":2,"i":6,"f":0.55},{"b":33.5,"et":4,"i":7,"f":0.45},{"b":33.75002268,"et":2,"i":7,"f":0.35},{"b":34.0,"et":1,"i":7,"f":0.75},{"b":34.0,"et":2,"i":5,"f":0.55},{"b":34.0,"et":3,"i":5,"f":0.55},{"b":34.0,"et":4,"i":5,"f":0.6},{"b":34.24997732,"et":2,"i":7,"f":0.35},{"b":34.5,"et":2,"i":6,"f":0.55},{"b":34.5,"et":4,"i":7,"f":0.45},{"b":34.75002268,"et":2,"i":7,"f":0.35},{"b":35.0,"et":2,"i":6,"f":0.55},{"b":35.0,"et":4,"i":7,"f":0.45},{"b":35.24997732,"et":2,"i":7,"f":0.35},{"b":35.5,"et":2,"i":6,"f":0.55},{"b":35.5,"et":4,"i":7,"f":0.45},{"b":35.75002268,"et":2,"i":7,"f":0.35},{"b":36.0,"et":0,"i":1,"f":0.95},{"b":36.0,"et":1,"i":2,"f":1.0},{"b":36.0,"et":2,"i":2,"f":0.85},{"b":36.0,"et":3,"i":2,"f":0.85},{"b":36.0,"et":4,"i":2,"f":0.95},{"b":36.24997732,"et":2,"i":3,"f":0.35},{"b":36.5,"et":2,"i":2,"f":0.55},{"b":36.5,"et":4,"i":3,"f":0.45},{"b":36.75002268,"et":2,"i":3,"f":0.35},{"b":37.0,"et":2,"i":2,"f":0.55},{"b":37.0,"et":4,"i":3,"f":0.45},{"b":37.24997732,"et":2,"i":3,"f":0.35},{"b":37.5,"et":2,"i":2,"f":0.55},{"b":37.5,"et":4,"i":3,"f":0.45},{"b":37.75002268,"et":2,"i":3,"f":0.35},{"b":38.0,"et":1,"i":3,"f":0.75},{"b":38.0,"et":2,"i":1,"f":0.55},{"b":38.0,"et":3,"i":1,"f":0.55},{"b":38.0,"et":4,"i":1,"f":0.6},{"b":38.24997732,"et":2,"i":3,"f":0.35},{"b":38.5,"et":2,"i":2,"f":0.55},{"b":38.5,"et":4,"i":3,"f":0.45},{"b":38.75002268,"et":2,"i":3,"f":0.35},{"b":39.0,"et":2,"i":2,"f":0.55},{"b":39.0,"et":4,"i":3,"f":0.45},{"b":39.24997732,"et":2,"i":3,"f":0.35},{"b":39.5,"et":2,"i":2,"f":0.55},{"b":39.5,"et":4,"i":3,"f":0.45},{"b":39.75002268,"et":2,"i":3,"f":0.35},{"b":40.0,"et":0,"i":5,"f":0.95},{"b":40.0,"et":1,"i":6,"f":1.0},{"b":40.0,"et":2,"i":6,"f":0.85},{"b":40.0,"et":3,"i":6,"f":0.85},{"b":40.0,"et":4,"i":6,"f":0.95},{"b":40.24997732,"et":2,"i":7,"f":0.35},{"b":40.5,"et":2,"i":6,"f":0.55},{"b":40.5,"et":4,"i":7,"f":0.45},{"b":40.75002268,"et":2,"i":7,"f":0.35},{"b":41.0,"et":2,"i":6,"f":0.55},{"b":41.0,"et":4,"i":7,"f":0.45},{"b":41.24997732,"et":2,"i":7,"f":0.35},{"b":41.5,"et":2,"i":6,"f":0.55},{"b":41.5,"et":4,"i":7,"f":0.45},{"b":41.75002268,"et":2,"i":7,"f":0.35},{"b":42.0,"et":1,"i":7,"f":0.75},{"b":42.0,"et":2,"i":5,"f":0.55},{"b":42.0,"et":3,"i":5,"f":0.55},{"b":42.0,"et":4,"i":5,"f":0.6},{"b":42.24997732,"et":2,"i":7,"f":0.35},{"b":42.5,"et":2,"i":6,"f":0.55},{"b":42.5,"et":4,"i":7,"f":0.45},{"b":42.75002268,"et":2,"i":7,"f":0.35},{"b":43.0,"et":2,"i":6,"f":0.55},{"b":43.0,"et":4,"i":7,"f":0.45},{"b":43.24997732,"et":2,"i":7,"f":0.35},{"b":43.5,"et":2,"i":6,"f":0.55},{"b":43.5,"et":4,"i":7,"f":0.45},{"b":43.75002268,"et":2,"i":7,"f":0.35},{"b":44.0,"et":0,"i":1,"f":0.65},{"b":44.0,"et":1,"i":2,"f":1.0},{"b":44.0,"et":4,"i":1,"f":0.4},{"b":44.5,"et":2,"i":3,"f":0.32},{"b":45.0,"et":2,"i":3,"f":0.32},{"b":45.5,"et":2,"i":3,"f":0.32},{"b":46.0,"et":1,"i":3,"f":0.4},{"b":46.0,"et":4,"i":1,"f":0.4},{"b":46.5,"et":2,"i":3,"f":0.32},{"b":47.0,"et":2,"i":3,"f":0.32},{"b":47.5,"et":2,"i":3,"f":0.32},{"b":48.0,"et":0,"i":5,"f":0.95},{"b":48.0,"et":1,"i":6,"f":1.0},{"b":48.0,"et":2,"i":6,"f":0.85},{"b":48.0,"et":3,"i":6,"f":0.85},{"b":48.0,"et":4,"i":6,"f":0.95},{"b":48.24997732,"et":2,"i":7,"f":0.35},{"b":48.5,"et":2,"i":6,"f":0.55},{"b":48.5,"et":4,"i":7,"f":0.45},{"b":48.75002268,"et":2,"i":7,"f":0.35},{"b":49.0,"et":2,"i":6,"f":0.55},{"b":49.0,"et":4,"i":7,"f":0.45},{"b":49.24997732,"et":2,"i":7,"f":0.35},{"b":49.5,"et":2,"i":6,"f":0.55},{"b":49.5,"et":4,"i":7,"f":0.45},{"b":49.75002268,"et":2,"i":7,"f":0.35},{"b":50.0,"et":1,"i":7,"f":0.75},{"b":50.0,"et":2,"i":5,"f":0.55},{"b":50.0,"et":3,"i":5,"f":0.55},{"b":50.0,"et":4,"i":5,"f":0.6},{"b":50.24997732,"et":2,"i":7,"f":0.35},{"b":50.5,"et":2,"i":6,"f":0.55},{"b":50.5,"et":4,"i":7,"f":0.45},{"b":50.75002268,"et":2,"i":7,"f":0.35},{"b":51.0,"et":2,"i":6,"f":0.55},{"b":51.0,"et":4,"i":7,"f":0.45},{"b":51.24997732,"et":2,"i":7,"f":0.35},{"b":51.5,"et":2,"i":6,"f":0.55},{"b":51.5,"et":4,"i":7,"f":0.45},{"b":51.75002268,"et":2,"i":7,"f":0.35},{"b":52.0,"et":0,"i":1,"f":0.95},{"b":52.0,"et":1,"i":2,"f":1.0},{"b":52.0,"et":2,"i":2,"f":0.85},{"b":52.0,"et":3,"i":2,"f":0.85},{"b":52.0,"et":4,"i":2,"f":0.95},{"b":52.24997732,"et":2,"i":3,"f":0.35},{"b":52.5,"et":2,"i":2,"f":0.55},{"b":52.5,"et":4,"i":3,"f":0.45},{"b":52.75002268,"et":2,"i":3,"f":0.35},{"b":53.0,"et":2,"i":2,"f":0.55},{"b":53.0,"et":4,"i":3,"f":0.45},{"b":53.24997732,"et":2,"i":3,"f":0.35},{"b":53.5,"et":2,"i":2,"f":0.55},{"b":53.5,"et":4,"i":3,"f":0.45},{"b":53.75002268,"et":2,"i":3,"f":0.35},{"b":54.0,"et":1,"i":3,"f":0.75},{"b":54.0,"et":2,"i":1,"f":0.55},{"b":54.0,"et":3,"i":1,"f":0.55},{"b":54.0,"et":4,"i":1,"f":0.6},{"b":54.24997732,"et":2,"i":3,"f":0.35},{"b":54.5,"et":2,"i":2,"f":0.55},{"b":54.5,"et":4,"i":3,"f":0.45},{"b":54.75002268,"et":2,"i":3,"f":0.35},{"b":55.0,"et":2,"i":2,"f":0.55},{"b":55.0,"et":4,"i":3,"f":0.45},{"b":55.24997732,"et":2,"i":3,"f":0.35},{"b":55.5,"et":2,"i":2,"f":0.55},{"b":55.5,"et":4,"i":3,"f":0.45},{"b":55.75002268,"et":2,"i":3,"f":0.35},{"b":56.0,"et":0,"i":5,"f":0.95},{"b":56.0,"et":1,"i":6,"f":1.0},{"b":56.0,"et":2,"i":6,"f":0.85},{"b":56.0,"et":3,"i":6,"f":0.85},{"b":56.0,"et":4,"i":6,"f":0.95},{"b":56.24997732,"et":2,"i":7,"f":0.35},{"b":56.5,"et":2,"i":6,"f":0.55},{"b":56.5,"et":4,"i":7,"f":0.45},{"b":56.75002268,"et":2,"i":7,"f":0.35},{"b":57.0,"et":2,"i":6,"f":0.55},{"b":57.0,"et":4,"i":7,"f":0.45},{"b":57.24997732,"et":2,"i":7,"f":0.35},{"b":57.5,"et":2,"i":6,"f":0.55},{"b":57.5,"et":4,"i":7,"f":0.45},{"b":57.75002268,"et":2,"i":7,"f":0.35},{"b":58.0,"et":1,"i":7,"f":0.75},{"b":58.0,"et":2,"i":5,"f":0.55},{"b":58.0,"et":3,"i":5,"f":0.55},{"b":58.0,"et":4,"i":5,"f":0.6},{"b":58.24997732,"et":2,"i":7,"f":0.35},{"b":58.5,"et":2,"i":6,"f":0.55},{"b":58.5,"et":4,"i":7,"f":0.45},{"b":58.75002268,"et":2,"i":7,"f":0.35},{"b":59.0,"et":2,"i":6,"f":0.55},{"b":59.0,"et":4,"i":7,"f":0.45},{"b":59.24997732,"et":2,"i":7,"f":0.35},{"b":59.5,"et":2,"i":6,"f":0.55},{"b":59.5,"et":4,"i":7,"f":0.45},{"b":59.75002268,"et":2,"i":7,"f":0.35},{"b":60.0,"et":0,"i":1,"f":0.95},{"b":60.0,"et":1,"i":2,"f":1.0},{"b":60.0,"et":2,"i":2,"f":0.85},{"b":60.0,"et":3,"i":2,"f":0.85},{"b":60.0,"et":4,"i":2,"f":0.95}],"colorBoostBeatmapEvents":[{"b":4.0,"o":true},{"b":8.0,"o":false},{"b":24.0,"o":true},{"b":44.0,"o":false},{"b":48.0,"o":true}],"lightColorEventBoxGroups":[{"b":0.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":24.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":44.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":48.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":4.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":8.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":12.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":16.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":20.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":44.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]}],"lightRotationEventBoxGroups":[{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]}],"lightTranslationEventBoxGroups":[{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]}],"vfxEventBoxGroups":[],"basicEventTypesWithKeywords":{},"useNormalEventsAsCompatibleEvents":true,"customData":{"_beatforgeTiming":{"schemaVersion":1,"clock":"constant-bpm-sample-projection","sampleRate":44100,"bpm":120.0}}},"chartHash":"a6bf9087249c799a91068795bc60396a0199cb805d34d218419616c12efb27be","audioHash":"41847c93d9f7a502b34f11ca4d099028816ebc4e5c9eb67eaee6f9a8f9b69c05","sections":[{"id":"section-000","label":"Intro","startBeat":0.0,"endBeat":8.0,"intensity":0.3,"motifId":"intro","motifOffset":0,"density":0.95,"dominantInstrument":"auto","style":"flow","authoredStartBeat":0,"authoredEndBeat":8,"startSeconds":0.0,"endSeconds":4.0},{"id":"section-001","label":"Verse","startBeat":8.0,"endBeat":24.0,"intensity":0.5,"motifId":"verse","motifOffset":3,"density":0.8075,"dominantInstrument":"auto","style":"flow","authoredStartBeat":8,"authoredEndBeat":24,"startSeconds":4.0,"endSeconds":12.0},{"id":"section-002","label":"Chorus","startBeat":24.0,"endBeat":40.0,"intensity":0.82,"motifId":"chorus","motifOffset":3,"density":1.0925,"dominantInstrument":"auto","style":"flow","authoredStartBeat":24,"authoredEndBeat":40,"startSeconds":12.0,"endSeconds":20.0},{"id":"section-003","label":"Verse","startBeat":40.0,"endBeat":48.0,"intensity":0.5,"motifId":"verse","motifOffset":3,"density":0.8075,"dominantInstrument":"auto","style":"flow","authoredStartBeat":40,"authoredEndBeat":48,"startSeconds":20.0,"endSeconds":24.0},{"id":"section-004","label":"Chorus","startBeat":48.0,"endBeat":60.0,"intensity":0.82,"motifId":"chorus","motifOffset":3,"density":1.0925,"dominantInstrument":"auto","style":"flow","authoredStartBeat":48,"authoredEndBeat":60,"startSeconds":24.0,"endSeconds":30.0}],"findings":[],"mappingPlan":{"schemaVersion":1,"source":"synthetic-authored-score","sections":[{"id":"section-000","label":"Intro","startBeat":0.0,"endBeat":8.0,"intensity":0.3,"motifId":"intro","motifOffset":0,"density":0.95,"dominantInstrument":"auto","style":"flow","authoredStartBeat":0,"authoredEndBeat":8,"startSeconds":0.0,"endSeconds":4.0},{"id":"section-001","label":"Verse","startBeat":8.0,"endBeat":24.0,"intensity":0.5,"motifId":"verse","motifOffset":3,"density":0.8075,"dominantInstrument":"auto","style":"flow","authoredStartBeat":8,"authoredEndBeat":24,"startSeconds":4.0,"endSeconds":12.0},{"id":"section-002","label":"Chorus","startBeat":24.0,"endBeat":40.0,"intensity":0.82,"motifId":"chorus","motifOffset":3,"density":1.0925,"dominantInstrument":"auto","style":"flow","authoredStartBeat":24,"authoredEndBeat":40,"startSeconds":12.0,"endSeconds":20.0},{"id":"section-003","label":"Verse","startBeat":40.0,"endBeat":48.0,"intensity":0.5,"motifId":"verse","motifOffset":3,"density":0.8075,"dominantInstrument":"auto","style":"flow","authoredStartBeat":40,"authoredEndBeat":48,"startSeconds":20.0,"endSeconds":24.0},{"id":"section-004","label":"Chorus","startBeat":48.0,"endBeat":60.0,"intensity":0.82,"motifId":"chorus","motifOffset":3,"density":1.0925,"dominantInstrument":"auto","style":"flow","authoredStartBeat":48,"authoredEndBeat":60,"startSeconds":24.0,"endSeconds":30.0}],"controls":{"schemaVersion":1,"brief":"Flowing patterns, lighter verses, denser choruses","intensity":1.0,"density":1.0,"verseDensity":0.85,"chorusDensity":1.15,"dominantInstrument":"auto","style":"flow","noBombs":false,"noWalls":false,"candidateCount":2,"sectionOverrides":[],"interpretation":[]},"exportClock":"constant-bpm-sample-projection"},"provenance":{"schemaVersion":1,"source":"synthetic-authored-score","seed":20260911,"engine":"canonical-choreography","model":null,"mappingPlan":{"schemaVersion":1,"source":"synthetic-authored-score","sections":[{"id":"section-000","label":"Intro","startBeat":0.0,"endBeat":8.0,"intensity":0.3,"motifId":"intro","motifOffset":0,"density":0.95,"dominantInstrument":"auto","style":"flow","authoredStartBeat":0,"authoredEndBeat":8,"startSeconds":0.0,"endSeconds":4.0},{"id":"section-001","label":"Verse","startBeat":8.0,"endBeat":24.0,"intensity":0.5,"motifId":"verse","motifOffset":3,"density":0.8075,"dominantInstrument":"auto","style":"flow","authoredStartBeat":8,"authoredEndBeat":24,"startSeconds":4.0,"endSeconds":12.0},{"id":"section-002","label":"Chorus","startBeat":24.0,"endBeat":40.0,"intensity":0.82,"motifId":"chorus","motifOffset":3,"density":1.0925,"dominantInstrument":"auto","style":"flow","authoredStartBeat":24,"authoredEndBeat":40,"startSeconds":12.0,"endSeconds":20.0},{"id":"section-003","label":"Verse","startBeat":40.0,"endBeat":48.0,"intensity":0.5,"motifId":"verse","motifOffset":3,"density":0.8075,"dominantInstrument":"auto","style":"flow","authoredStartBeat":40,"authoredEndBeat":48,"startSeconds":20.0,"endSeconds":24.0},{"id":"section-004","label":"Chorus","startBeat":48.0,"endBeat":60.0,"intensity":0.82,"motifId":"chorus","motifOffset":3,"density":1.0925,"dominantInstrument":"auto","style":"flow","authoredStartBeat":48,"authoredEndBeat":60,"startSeconds":24.0,"endSeconds":30.0}],"controls":{"schemaVersion":1,"brief":"Flowing patterns, lighter verses, denser choruses","intensity":1.0,"density":1.0,"verseDensity":0.85,"chorusDensity":1.15,"dominantInstrument":"auto","style":"flow","noBombs":false,"noWalls":false,"candidateCount":2,"sectionOverrides":[],"interpretation":[]},"exportClock":"constant-bpm-sample-projection"},"timing":{"method":"Every oscillator and drum is placed at round(beat * 60 / bpm * sampleRate)","authoredSampleClock":true,"trackerAnalysisPerformed":false,"humanListeningVerified":false},"officialCorpusUsed":false,"audio":{"originalSynthesis":true,"sampleRate":44100,"samples":1323000,"durationSeconds":30.0,"sha256":"41847c93d9f7a502b34f11ca4d099028816ebc4e5c9eb67eaee6f9a8f9b69c05"},"releaseGate":{"structuralInspection":true,"fullSpeedVrPlaytest":false,"slowVrPlaytest":false,"freshSightRead":false},"humanPlaytests":[]},"colors":{"left":{"r":0.98,"g":0.62,"b":0.08,"a":1.0},"right":{"r":0.08,"g":0.88,"b":0.98,"a":1.0}},"timingVerified":true,"source":"synthetic-authored-score","charts":{"Easy":{"version":"3.3.0","bpmEvents":[],"rotationEvents":[],"colorNotes":[{"b":4.0,"x":1,"y":0,"c":0,"d":4,"a":0},{"b":8.0,"x":3,"y":2,"c":1,"d":4,"a":0},{"b":12.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":16.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":20.0,"x":1,"y":0,"c":1,"d":5,"a":0},{"b":24.0,"x":0,"y":2,"c":0,"d":5,"a":0},{"b":27.0,"x":3,"y":2,"c":1,"d":4,"a":0},{"b":29.0,"x":1,"y":0,"c":0,"d":4,"a":0},{"b":31.0,"x":3,"y":0,"c":1,"d":7,"a":0},{"b":33.0,"x":0,"y":0,"c":0,"d":7,"a":0},{"b":35.0,"x":3,"y":0,"c":1,"d":5,"a":0},{"b":37.0,"x":0,"y":0,"c":0,"d":5,"a":0},{"b":39.0,"x":2,"y":2,"c":1,"d":0,"a":0},{"b":44.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":48.0,"x":1,"y":0,"c":0,"d":7,"a":0},{"b":51.0,"x":3,"y":0,"c":1,"d":7,"a":0},{"b":53.0,"x":1,"y":0,"c":0,"d":0,"a":0},{"b":55.0,"x":2,"y":2,"c":1,"d":0,"a":0},{"b":57.0,"x":0,"y":0,"c":0,"d":7,"a":0},{"b":59.0,"x":3,"y":0,"c":1,"d":7,"a":0}],"bombNotes":[],"obstacles":[{"b":8.5,"d":1.5,"x":0,"y":0,"w":2,"h":5},{"b":24.5,"d":1.5,"x":2,"y":0,"w":2,"h":5},{"b":44.5,"d":1.5,"x":0,"y":0,"w":2,"h":5},{"b":48.5,"d":1.5,"x":2,"y":0,"w":2,"h":5}],"sliders":[],"burstSliders":[],"waypoints":[],"basicBeatmapEvents":[{"b":0.0,"et":0,"i":5,"f":0.45},{"b":0.0,"et":1,"i":7,"f":0.35},{"b":4.0,"et":0,"i":1,"f":0.45},{"b":4.0,"et":1,"i":6,"f":1.0},{"b":8.0,"et":0,"i":5,"f":0.65},{"b":8.0,"et":1,"i":2,"f":1.0},{"b":8.0,"et":4,"i":5,"f":0.4},{"b":8.5,"et":2,"i":7,"f":0.32},{"b":9.0,"et":2,"i":7,"f":0.32},{"b":9.5,"et":2,"i":7,"f":0.32},{"b":10.0,"et":1,"i":7,"f":0.4},{"b":10.0,"et":4,"i":5,"f":0.4},{"b":10.5,"et":2,"i":7,"f":0.32},{"b":11.0,"et":2,"i":7,"f":0.32},{"b":11.5,"et":2,"i":7,"f":0.32},{"b":12.0,"et":0,"i":1,"f":0.65},{"b":12.0,"et":1,"i":3,"f":0.55},{"b":12.0,"et":4,"i":1,"f":0.4},{"b":12.5,"et":2,"i":3,"f":0.32},{"b":13.0,"et":2,"i":3,"f":0.32},{"b":13.5,"et":2,"i":3,"f":0.32},{"b":14.0,"et":1,"i":3,"f":0.4},{"b":14.0,"et":4,"i":1,"f":0.4},{"b":14.5,"et":2,"i":3,"f":0.32},{"b":15.0,"et":2,"i":3,"f":0.32},{"b":15.5,"et":2,"i":3,"f":0.32},{"b":16.0,"et":0,"i":5,"f":0.65},{"b":16.0,"et":1,"i":7,"f":0.55},{"b":16.0,"et":4,"i":5,"f":0.4},{"b":16.5,"et":2,"i":7,"f":0.32},{"b":17.0,"et":2,"i":7,"f":0.32},{"b":17.5,"et":2,"i":7,"f":0.32},{"b":18.0,"et":1,"i":7,"f":0.4},{"b":18.0,"et":4,"i":5,"f":0.4},{"b":18.5,"et":2,"i":7,"f":0.32},{"b":19.0,"et":2,"i":7,"f":0.32},{"b":19.5,"et":2,"i":7,"f":0.32},{"b":20.0,"et":0,"i":1,"f":0.65},{"b":20.0,"et":1,"i":3,"f":0.55},{"b":20.0,"et":4,"i":1,"f":0.4},{"b":20.5,"et":2,"i":3,"f":0.32},{"b":21.0,"et":2,"i":3,"f":0.32},{"b":21.5,"et":2,"i":3,"f":0.32},{"b":22.0,"et":1,"i":3,"f":0.4},{"b":22.0,"et":4,"i":1,"f":0.4},{"b":22.5,"et":2,"i":3,"f":0.32},{"b":23.0,"et":2,"i":3,"f":0.32},{"b":23.5,"et":2,"i":3,"f":0.32},{"b":24.0,"et":0,"i":5,"f":0.95},{"b":24.0,"et":1,"i":6,"f":1.0},{"b":24.0,"et":2,"i":6,"f":0.85},{"b":24.0,"et":3,"i":6,"f":0.85},{"b":24.0,"et":4,"i":6,"f":0.95},{"b":24.24997732,"et":2,"i":7,"f":0.35},{"b":24.5,"et":2,"i":6,"f":0.55},{"b":24.5,"et":4,"i":7,"f":0.45},{"b":24.75002268,"et":2,"i":7,"f":0.35},{"b":25.0,"et":2,"i":6,"f":0.55},{"b":25.0,"et":4,"i":7,"f":0.45},{"b":25.24997732,"et":2,"i":7,"f":0.35},{"b":25.5,"et":2,"i":6,"f":0.55},{"b":25.5,"et":4,"i":7,"f":0.45},{"b":25.75002268,"et":2,"i":7,"f":0.35},{"b":26.0,"et":1,"i":7,"f":0.75},{"b":26.0,"et":2,"i":5,"f":0.55},{"b":26.0,"et":3,"i":5,"f":0.55},{"b":26.0,"et":4,"i":5,"f":0.6},{"b":26.24997732,"et":2,"i":7,"f":0.35},{"b":26.5,"et":2,"i":6,"f":0.55},{"b":26.5,"et":4,"i":7,"f":0.45},{"b":26.75002268,"et":2,"i":7,"f":0.35},{"b":27.0,"et":2,"i":6,"f":0.55},{"b":27.0,"et":4,"i":7,"f":0.45},{"b":27.24997732,"et":2,"i":7,"f":0.35},{"b":27.5,"et":2,"i":6,"f":0.55},{"b":27.5,"et":4,"i":7,"f":0.45},{"b":27.75002268,"et":2,"i":7,"f":0.35},{"b":28.0,"et":0,"i":1,"f":0.95},{"b":28.0,"et":1,"i":2,"f":1.0},{"b":28.0,"et":2,"i":2,"f":0.85},{"b":28.0,"et":3,"i":2,"f":0.85},{"b":28.0,"et":4,"i":2,"f":0.95},{"b":28.24997732,"et":2,"i":3,"f":0.35},{"b":28.5,"et":2,"i":2,"f":0.55},{"b":28.5,"et":4,"i":3,"f":0.45},{"b":28.75002268,"et":2,"i":3,"f":0.35},{"b":29.0,"et":2,"i":2,"f":0.55},{"b":29.0,"et":4,"i":3,"f":0.45},{"b":29.24997732,"et":2,"i":3,"f":0.35},{"b":29.5,"et":2,"i":2,"f":0.55},{"b":29.5,"et":4,"i":3,"f":0.45},{"b":29.75002268,"et":2,"i":3,"f":0.35},{"b":30.0,"et":1,"i":3,"f":0.75},{"b":30.0,"et":2,"i":1,"f":0.55},{"b":30.0,"et":3,"i":1,"f":0.55},{"b":30.0,"et":4,"i":1,"f":0.6},{"b":30.24997732,"et":2,"i":3,"f":0.35},{"b":30.5,"et":2,"i":2,"f":0.55},{"b":30.5,"et":4,"i":3,"f":0.45},{"b":30.75002268,"et":2,"i":3,"f":0.35},{"b":31.0,"et":2,"i":2,"f":0.55},{"b":31.0,"et":4,"i":3,"f":0.45},{"b":31.24997732,"et":2,"i":3,"f":0.35},{"b":31.5,"et":2,"i":2,"f":0.55},{"b":31.5,"et":4,"i":3,"f":0.45},{"b":31.75002268,"et":2,"i":3,"f":0.35},{"b":32.0,"et":0,"i":5,"f":0.95},{"b":32.0,"et":1,"i":6,"f":1.0},{"b":32.0,"et":2,"i":6,"f":0.85},{"b":32.0,"et":3,"i":6,"f":0.85},{"b":32.0,"et":4,"i":6,"f":0.95},{"b":32.24997732,"et":2,"i":7,"f":0.35},{"b":32.5,"et":2,"i":6,"f":0.55},{"b":32.5,"et":4,"i":7,"f":0.45},{"b":32.75002268,"et":2,"i":7,"f":0.35},{"b":33.0,"et":2,"i":6,"f":0.55},{"b":33.0,"et":4,"i":7,"f":0.45},{"b":33.24997732,"et":2,"i":7,"f":0.35},{"b":33.5,"et":2,"i":6,"f":0.55},{"b":33.5,"et":4,"i":7,"f":0.45},{"b":33.75002268,"et":2,"i":7,"f":0.35},{"b":34.0,"et":1,"i":7,"f":0.75},{"b":34.0,"et":2,"i":5,"f":0.55},{"b":34.0,"et":3,"i":5,"f":0.55},{"b":34.0,"et":4,"i":5,"f":0.6},{"b":34.24997732,"et":2,"i":7,"f":0.35},{"b":34.5,"et":2,"i":6,"f":0.55},{"b":34.5,"et":4,"i":7,"f":0.45},{"b":34.75002268,"et":2,"i":7,"f":0.35},{"b":35.0,"et":2,"i":6,"f":0.55},{"b":35.0,"et":4,"i":7,"f":0.45},{"b":35.24997732,"et":2,"i":7,"f":0.35},{"b":35.5,"et":2,"i":6,"f":0.55},{"b":35.5,"et":4,"i":7,"f":0.45},{"b":35.75002268,"et":2,"i":7,"f":0.35},{"b":36.0,"et":0,"i":1,"f":0.95},{"b":36.0,"et":1,"i":2,"f":1.0},{"b":36.0,"et":2,"i":2,"f":0.85},{"b":36.0,"et":3,"i":2,"f":0.85},{"b":36.0,"et":4,"i":2,"f":0.95},{"b":36.24997732,"et":2,"i":3,"f":0.35},{"b":36.5,"et":2,"i":2,"f":0.55},{"b":36.5,"et":4,"i":3,"f":0.45},{"b":36.75002268,"et":2,"i":3,"f":0.35},{"b":37.0,"et":2,"i":2,"f":0.55},{"b":37.0,"et":4,"i":3,"f":0.45},{"b":37.24997732,"et":2,"i":3,"f":0.35},{"b":37.5,"et":2,"i":2,"f":0.55},{"b":37.5,"et":4,"i":3,"f":0.45},{"b":37.75002268,"et":2,"i":3,"f":0.35},{"b":38.0,"et":1,"i":3,"f":0.75},{"b":38.0,"et":2,"i":1,"f":0.55},{"b":38.0,"et":3,"i":1,"f":0.55},{"b":38.0,"et":4,"i":1,"f":0.6},{"b":38.24997732,"et":2,"i":3,"f":0.35},{"b":38.5,"et":2,"i":2,"f":0.55},{"b":38.5,"et":4,"i":3,"f":0.45},{"b":38.75002268,"et":2,"i":3,"f":0.35},{"b":39.0,"et":2,"i":2,"f":0.55},{"b":39.0,"et":4,"i":3,"f":0.45},{"b":39.24997732,"et":2,"i":3,"f":0.35},{"b":39.5,"et":2,"i":2,"f":0.55},{"b":39.5,"et":4,"i":3,"f":0.45},{"b":39.75002268,"et":2,"i":3,"f":0.35},{"b":40.0,"et":0,"i":5,"f":0.95},{"b":40.0,"et":1,"i":6,"f":1.0},{"b":40.0,"et":2,"i":6,"f":0.85},{"b":40.0,"et":3,"i":6,"f":0.85},{"b":40.0,"et":4,"i":6,"f":0.95},{"b":40.24997732,"et":2,"i":7,"f":0.35},{"b":40.5,"et":2,"i":6,"f":0.55},{"b":40.5,"et":4,"i":7,"f":0.45},{"b":40.75002268,"et":2,"i":7,"f":0.35},{"b":41.0,"et":2,"i":6,"f":0.55},{"b":41.0,"et":4,"i":7,"f":0.45},{"b":41.24997732,"et":2,"i":7,"f":0.35},{"b":41.5,"et":2,"i":6,"f":0.55},{"b":41.5,"et":4,"i":7,"f":0.45},{"b":41.75002268,"et":2,"i":7,"f":0.35},{"b":42.0,"et":1,"i":7,"f":0.75},{"b":42.0,"et":2,"i":5,"f":0.55},{"b":42.0,"et":3,"i":5,"f":0.55},{"b":42.0,"et":4,"i":5,"f":0.6},{"b":42.24997732,"et":2,"i":7,"f":0.35},{"b":42.5,"et":2,"i":6,"f":0.55},{"b":42.5,"et":4,"i":7,"f":0.45},{"b":42.75002268,"et":2,"i":7,"f":0.35},{"b":43.0,"et":2,"i":6,"f":0.55},{"b":43.0,"et":4,"i":7,"f":0.45},{"b":43.24997732,"et":2,"i":7,"f":0.35},{"b":43.5,"et":2,"i":6,"f":0.55},{"b":43.5,"et":4,"i":7,"f":0.45},{"b":43.75002268,"et":2,"i":7,"f":0.35},{"b":44.0,"et":0,"i":1,"f":0.65},{"b":44.0,"et":1,"i":2,"f":1.0},{"b":44.0,"et":4,"i":1,"f":0.4},{"b":44.5,"et":2,"i":3,"f":0.32},{"b":45.0,"et":2,"i":3,"f":0.32},{"b":45.5,"et":2,"i":3,"f":0.32},{"b":46.0,"et":1,"i":3,"f":0.4},{"b":46.0,"et":4,"i":1,"f":0.4},{"b":46.5,"et":2,"i":3,"f":0.32},{"b":47.0,"et":2,"i":3,"f":0.32},{"b":47.5,"et":2,"i":3,"f":0.32},{"b":48.0,"et":0,"i":5,"f":0.95},{"b":48.0,"et":1,"i":6,"f":1.0},{"b":48.0,"et":2,"i":6,"f":0.85},{"b":48.0,"et":3,"i":6,"f":0.85},{"b":48.0,"et":4,"i":6,"f":0.95},{"b":48.24997732,"et":2,"i":7,"f":0.35},{"b":48.5,"et":2,"i":6,"f":0.55},{"b":48.5,"et":4,"i":7,"f":0.45},{"b":48.75002268,"et":2,"i":7,"f":0.35},{"b":49.0,"et":2,"i":6,"f":0.55},{"b":49.0,"et":4,"i":7,"f":0.45},{"b":49.24997732,"et":2,"i":7,"f":0.35},{"b":49.5,"et":2,"i":6,"f":0.55},{"b":49.5,"et":4,"i":7,"f":0.45},{"b":49.75002268,"et":2,"i":7,"f":0.35},{"b":50.0,"et":1,"i":7,"f":0.75},{"b":50.0,"et":2,"i":5,"f":0.55},{"b":50.0,"et":3,"i":5,"f":0.55},{"b":50.0,"et":4,"i":5,"f":0.6},{"b":50.24997732,"et":2,"i":7,"f":0.35},{"b":50.5,"et":2,"i":6,"f":0.55},{"b":50.5,"et":4,"i":7,"f":0.45},{"b":50.75002268,"et":2,"i":7,"f":0.35},{"b":51.0,"et":2,"i":6,"f":0.55},{"b":51.0,"et":4,"i":7,"f":0.45},{"b":51.24997732,"et":2,"i":7,"f":0.35},{"b":51.5,"et":2,"i":6,"f":0.55},{"b":51.5,"et":4,"i":7,"f":0.45},{"b":51.75002268,"et":2,"i":7,"f":0.35},{"b":52.0,"et":0,"i":1,"f":0.95},{"b":52.0,"et":1,"i":2,"f":1.0},{"b":52.0,"et":2,"i":2,"f":0.85},{"b":52.0,"et":3,"i":2,"f":0.85},{"b":52.0,"et":4,"i":2,"f":0.95},{"b":52.24997732,"et":2,"i":3,"f":0.35},{"b":52.5,"et":2,"i":2,"f":0.55},{"b":52.5,"et":4,"i":3,"f":0.45},{"b":52.75002268,"et":2,"i":3,"f":0.35},{"b":53.0,"et":2,"i":2,"f":0.55},{"b":53.0,"et":4,"i":3,"f":0.45},{"b":53.24997732,"et":2,"i":3,"f":0.35},{"b":53.5,"et":2,"i":2,"f":0.55},{"b":53.5,"et":4,"i":3,"f":0.45},{"b":53.75002268,"et":2,"i":3,"f":0.35},{"b":54.0,"et":1,"i":3,"f":0.75},{"b":54.0,"et":2,"i":1,"f":0.55},{"b":54.0,"et":3,"i":1,"f":0.55},{"b":54.0,"et":4,"i":1,"f":0.6},{"b":54.24997732,"et":2,"i":3,"f":0.35},{"b":54.5,"et":2,"i":2,"f":0.55},{"b":54.5,"et":4,"i":3,"f":0.45},{"b":54.75002268,"et":2,"i":3,"f":0.35},{"b":55.0,"et":2,"i":2,"f":0.55},{"b":55.0,"et":4,"i":3,"f":0.45},{"b":55.24997732,"et":2,"i":3,"f":0.35},{"b":55.5,"et":2,"i":2,"f":0.55},{"b":55.5,"et":4,"i":3,"f":0.45},{"b":55.75002268,"et":2,"i":3,"f":0.35},{"b":56.0,"et":0,"i":5,"f":0.95},{"b":56.0,"et":1,"i":6,"f":1.0},{"b":56.0,"et":2,"i":6,"f":0.85},{"b":56.0,"et":3,"i":6,"f":0.85},{"b":56.0,"et":4,"i":6,"f":0.95},{"b":56.24997732,"et":2,"i":7,"f":0.35},{"b":56.5,"et":2,"i":6,"f":0.55},{"b":56.5,"et":4,"i":7,"f":0.45},{"b":56.75002268,"et":2,"i":7,"f":0.35},{"b":57.0,"et":2,"i":6,"f":0.55},{"b":57.0,"et":4,"i":7,"f":0.45},{"b":57.24997732,"et":2,"i":7,"f":0.35},{"b":57.5,"et":2,"i":6,"f":0.55},{"b":57.5,"et":4,"i":7,"f":0.45},{"b":57.75002268,"et":2,"i":7,"f":0.35},{"b":58.0,"et":1,"i":7,"f":0.75},{"b":58.0,"et":2,"i":5,"f":0.55},{"b":58.0,"et":3,"i":5,"f":0.55},{"b":58.0,"et":4,"i":5,"f":0.6},{"b":58.24997732,"et":2,"i":7,"f":0.35},{"b":58.5,"et":2,"i":6,"f":0.55},{"b":58.5,"et":4,"i":7,"f":0.45},{"b":58.75002268,"et":2,"i":7,"f":0.35},{"b":59.0,"et":2,"i":6,"f":0.55},{"b":59.0,"et":4,"i":7,"f":0.45},{"b":59.24997732,"et":2,"i":7,"f":0.35},{"b":59.5,"et":2,"i":6,"f":0.55},{"b":59.5,"et":4,"i":7,"f":0.45},{"b":59.75002268,"et":2,"i":7,"f":0.35},{"b":60.0,"et":0,"i":1,"f":0.95},{"b":60.0,"et":1,"i":2,"f":1.0},{"b":60.0,"et":2,"i":2,"f":0.85},{"b":60.0,"et":3,"i":2,"f":0.85},{"b":60.0,"et":4,"i":2,"f":0.95}],"colorBoostBeatmapEvents":[{"b":4.0,"o":true},{"b":8.0,"o":false},{"b":24.0,"o":true},{"b":44.0,"o":false},{"b":48.0,"o":true}],"lightColorEventBoxGroups":[{"b":0.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":24.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":44.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":48.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]}],"lightRotationEventBoxGroups":[{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]}],"lightTranslationEventBoxGroups":[{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]}],"vfxEventBoxGroups":[],"basicEventTypesWithKeywords":{},"useNormalEventsAsCompatibleEvents":true,"customData":{"_beatforgeTiming":{"schemaVersion":1,"clock":"constant-bpm-sample-projection","sampleRate":44100,"bpm":120.0}}},"Normal":{"version":"3.3.0","bpmEvents":[],"rotationEvents":[],"colorNotes":[{"b":4.0,"x":2,"y":1,"c":1,"d":7,"a":0},{"b":8.0,"x":1,"y":0,"c":0,"d":7,"a":0},{"b":12.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":16.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":20.0,"x":3,"y":2,"c":1,"d":6,"a":0},{"b":24.0,"x":1,"y":0,"c":0,"d":6,"a":0},{"b":27.0,"x":3,"y":2,"c":1,"d":4,"a":0},{"b":29.0,"x":1,"y":0,"c":0,"d":4,"a":0},{"b":31.0,"x":3,"y":0,"c":1,"d":7,"a":0},{"b":33.0,"x":0,"y":0,"c":0,"d":7,"a":0},{"b":35.0,"x":3,"y":0,"c":1,"d":5,"a":0},{"b":37.0,"x":0,"y":0,"c":0,"d":5,"a":0},{"b":39.0,"x":2,"y":2,"c":1,"d":0,"a":0},{"b":44.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":48.0,"x":1,"y":0,"c":0,"d":7,"a":0},{"b":51.0,"x":3,"y":0,"c":1,"d":7,"a":0},{"b":53.0,"x":1,"y":0,"c":0,"d":1,"a":0},{"b":55.0,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":57.0,"x":0,"y":0,"c":0,"d":7,"a":0},{"b":59.0,"x":3,"y":0,"c":1,"d":7,"a":0}],"bombNotes":[{"b":5.0,"x":0,"y":2},{"b":34.0,"x":3,"y":2},{"b":58.0,"x":0,"y":2}],"obstacles":[{"b":8.5,"d":1.5,"x":0,"y":0,"w":2,"h":5},{"b":24.5,"d":1.5,"x":2,"y":0,"w":2,"h":5},{"b":44.5,"d":1.5,"x":0,"y":0,"w":2,"h":5},{"b":48.5,"d":1.5,"x":2,"y":0,"w":2,"h":5}],"sliders":[],"burstSliders":[],"waypoints":[],"basicBeatmapEvents":[{"b":0.0,"et":0,"i":5,"f":0.45},{"b":0.0,"et":1,"i":7,"f":0.35},{"b":4.0,"et":0,"i":1,"f":0.45},{"b":4.0,"et":1,"i":6,"f":1.0},{"b":8.0,"et":0,"i":5,"f":0.65},{"b":8.0,"et":1,"i":2,"f":1.0},{"b":8.0,"et":4,"i":5,"f":0.4},{"b":8.5,"et":2,"i":7,"f":0.32},{"b":9.0,"et":2,"i":7,"f":0.32},{"b":9.5,"et":2,"i":7,"f":0.32},{"b":10.0,"et":1,"i":7,"f":0.4},{"b":10.0,"et":4,"i":5,"f":0.4},{"b":10.5,"et":2,"i":7,"f":0.32},{"b":11.0,"et":2,"i":7,"f":0.32},{"b":11.5,"et":2,"i":7,"f":0.32},{"b":12.0,"et":0,"i":1,"f":0.65},{"b":12.0,"et":1,"i":3,"f":0.55},{"b":12.0,"et":4,"i":1,"f":0.4},{"b":12.5,"et":2,"i":3,"f":0.32},{"b":13.0,"et":2,"i":3,"f":0.32},{"b":13.5,"et":2,"i":3,"f":0.32},{"b":14.0,"et":1,"i":3,"f":0.4},{"b":14.0,"et":4,"i":1,"f":0.4},{"b":14.5,"et":2,"i":3,"f":0.32},{"b":15.0,"et":2,"i":3,"f":0.32},{"b":15.5,"et":2,"i":3,"f":0.32},{"b":16.0,"et":0,"i":5,"f":0.65},{"b":16.0,"et":1,"i":7,"f":0.55},{"b":16.0,"et":4,"i":5,"f":0.4},{"b":16.5,"et":2,"i":7,"f":0.32},{"b":17.0,"et":2,"i":7,"f":0.32},{"b":17.5,"et":2,"i":7,"f":0.32},{"b":18.0,"et":1,"i":7,"f":0.4},{"b":18.0,"et":4,"i":5,"f":0.4},{"b":18.5,"et":2,"i":7,"f":0.32},{"b":19.0,"et":2,"i":7,"f":0.32},{"b":19.5,"et":2,"i":7,"f":0.32},{"b":20.0,"et":0,"i":1,"f":0.65},{"b":20.0,"et":1,"i":3,"f":0.55},{"b":20.0,"et":4,"i":1,"f":0.4},{"b":20.5,"et":2,"i":3,"f":0.32},{"b":21.0,"et":2,"i":3,"f":0.32},{"b":21.5,"et":2,"i":3,"f":0.32},{"b":22.0,"et":1,"i":3,"f":0.4},{"b":22.0,"et":4,"i":1,"f":0.4},{"b":22.5,"et":2,"i":3,"f":0.32},{"b":23.0,"et":2,"i":3,"f":0.32},{"b":23.5,"et":2,"i":3,"f":0.32},{"b":24.0,"et":0,"i":5,"f":0.95},{"b":24.0,"et":1,"i":6,"f":1.0},{"b":24.0,"et":2,"i":6,"f":0.85},{"b":24.0,"et":3,"i":6,"f":0.85},{"b":24.0,"et":4,"i":6,"f":0.95},{"b":24.24997732,"et":2,"i":7,"f":0.35},{"b":24.5,"et":2,"i":6,"f":0.55},{"b":24.5,"et":4,"i":7,"f":0.45},{"b":24.75002268,"et":2,"i":7,"f":0.35},{"b":25.0,"et":2,"i":6,"f":0.55},{"b":25.0,"et":4,"i":7,"f":0.45},{"b":25.24997732,"et":2,"i":7,"f":0.35},{"b":25.5,"et":2,"i":6,"f":0.55},{"b":25.5,"et":4,"i":7,"f":0.45},{"b":25.75002268,"et":2,"i":7,"f":0.35},{"b":26.0,"et":1,"i":7,"f":0.75},{"b":26.0,"et":2,"i":5,"f":0.55},{"b":26.0,"et":3,"i":5,"f":0.55},{"b":26.0,"et":4,"i":5,"f":0.6},{"b":26.24997732,"et":2,"i":7,"f":0.35},{"b":26.5,"et":2,"i":6,"f":0.55},{"b":26.5,"et":4,"i":7,"f":0.45},{"b":26.75002268,"et":2,"i":7,"f":0.35},{"b":27.0,"et":2,"i":6,"f":0.55},{"b":27.0,"et":4,"i":7,"f":0.45},{"b":27.24997732,"et":2,"i":7,"f":0.35},{"b":27.5,"et":2,"i":6,"f":0.55},{"b":27.5,"et":4,"i":7,"f":0.45},{"b":27.75002268,"et":2,"i":7,"f":0.35},{"b":28.0,"et":0,"i":1,"f":0.95},{"b":28.0,"et":1,"i":2,"f":1.0},{"b":28.0,"et":2,"i":2,"f":0.85},{"b":28.0,"et":3,"i":2,"f":0.85},{"b":28.0,"et":4,"i":2,"f":0.95},{"b":28.24997732,"et":2,"i":3,"f":0.35},{"b":28.5,"et":2,"i":2,"f":0.55},{"b":28.5,"et":4,"i":3,"f":0.45},{"b":28.75002268,"et":2,"i":3,"f":0.35},{"b":29.0,"et":2,"i":2,"f":0.55},{"b":29.0,"et":4,"i":3,"f":0.45},{"b":29.24997732,"et":2,"i":3,"f":0.35},{"b":29.5,"et":2,"i":2,"f":0.55},{"b":29.5,"et":4,"i":3,"f":0.45},{"b":29.75002268,"et":2,"i":3,"f":0.35},{"b":30.0,"et":1,"i":3,"f":0.75},{"b":30.0,"et":2,"i":1,"f":0.55},{"b":30.0,"et":3,"i":1,"f":0.55},{"b":30.0,"et":4,"i":1,"f":0.6},{"b":30.24997732,"et":2,"i":3,"f":0.35},{"b":30.5,"et":2,"i":2,"f":0.55},{"b":30.5,"et":4,"i":3,"f":0.45},{"b":30.75002268,"et":2,"i":3,"f":0.35},{"b":31.0,"et":2,"i":2,"f":0.55},{"b":31.0,"et":4,"i":3,"f":0.45},{"b":31.24997732,"et":2,"i":3,"f":0.35},{"b":31.5,"et":2,"i":2,"f":0.55},{"b":31.5,"et":4,"i":3,"f":0.45},{"b":31.75002268,"et":2,"i":3,"f":0.35},{"b":32.0,"et":0,"i":5,"f":0.95},{"b":32.0,"et":1,"i":6,"f":1.0},{"b":32.0,"et":2,"i":6,"f":0.85},{"b":32.0,"et":3,"i":6,"f":0.85},{"b":32.0,"et":4,"i":6,"f":0.95},{"b":32.24997732,"et":2,"i":7,"f":0.35},{"b":32.5,"et":2,"i":6,"f":0.55},{"b":32.5,"et":4,"i":7,"f":0.45},{"b":32.75002268,"et":2,"i":7,"f":0.35},{"b":33.0,"et":2,"i":6,"f":0.55},{"b":33.0,"et":4,"i":7,"f":0.45},{"b":33.24997732,"et":2,"i":7,"f":0.35},{"b":33.5,"et":2,"i":6,"f":0.55},{"b":33.5,"et":4,"i":7,"f":0.45},{"b":33.75002268,"et":2,"i":7,"f":0.35},{"b":34.0,"et":1,"i":7,"f":0.75},{"b":34.0,"et":2,"i":5,"f":0.55},{"b":34.0,"et":3,"i":5,"f":0.55},{"b":34.0,"et":4,"i":5,"f":0.6},{"b":34.24997732,"et":2,"i":7,"f":0.35},{"b":34.5,"et":2,"i":6,"f":0.55},{"b":34.5,"et":4,"i":7,"f":0.45},{"b":34.75002268,"et":2,"i":7,"f":0.35},{"b":35.0,"et":2,"i":6,"f":0.55},{"b":35.0,"et":4,"i":7,"f":0.45},{"b":35.24997732,"et":2,"i":7,"f":0.35},{"b":35.5,"et":2,"i":6,"f":0.55},{"b":35.5,"et":4,"i":7,"f":0.45},{"b":35.75002268,"et":2,"i":7,"f":0.35},{"b":36.0,"et":0,"i":1,"f":0.95},{"b":36.0,"et":1,"i":2,"f":1.0},{"b":36.0,"et":2,"i":2,"f":0.85},{"b":36.0,"et":3,"i":2,"f":0.85},{"b":36.0,"et":4,"i":2,"f":0.95},{"b":36.24997732,"et":2,"i":3,"f":0.35},{"b":36.5,"et":2,"i":2,"f":0.55},{"b":36.5,"et":4,"i":3,"f":0.45},{"b":36.75002268,"et":2,"i":3,"f":0.35},{"b":37.0,"et":2,"i":2,"f":0.55},{"b":37.0,"et":4,"i":3,"f":0.45},{"b":37.24997732,"et":2,"i":3,"f":0.35},{"b":37.5,"et":2,"i":2,"f":0.55},{"b":37.5,"et":4,"i":3,"f":0.45},{"b":37.75002268,"et":2,"i":3,"f":0.35},{"b":38.0,"et":1,"i":3,"f":0.75},{"b":38.0,"et":2,"i":1,"f":0.55},{"b":38.0,"et":3,"i":1,"f":0.55},{"b":38.0,"et":4,"i":1,"f":0.6},{"b":38.24997732,"et":2,"i":3,"f":0.35},{"b":38.5,"et":2,"i":2,"f":0.55},{"b":38.5,"et":4,"i":3,"f":0.45},{"b":38.75002268,"et":2,"i":3,"f":0.35},{"b":39.0,"et":2,"i":2,"f":0.55},{"b":39.0,"et":4,"i":3,"f":0.45},{"b":39.24997732,"et":2,"i":3,"f":0.35},{"b":39.5,"et":2,"i":2,"f":0.55},{"b":39.5,"et":4,"i":3,"f":0.45},{"b":39.75002268,"et":2,"i":3,"f":0.35},{"b":40.0,"et":0,"i":5,"f":0.95},{"b":40.0,"et":1,"i":6,"f":1.0},{"b":40.0,"et":2,"i":6,"f":0.85},{"b":40.0,"et":3,"i":6,"f":0.85},{"b":40.0,"et":4,"i":6,"f":0.95},{"b":40.24997732,"et":2,"i":7,"f":0.35},{"b":40.5,"et":2,"i":6,"f":0.55},{"b":40.5,"et":4,"i":7,"f":0.45},{"b":40.75002268,"et":2,"i":7,"f":0.35},{"b":41.0,"et":2,"i":6,"f":0.55},{"b":41.0,"et":4,"i":7,"f":0.45},{"b":41.24997732,"et":2,"i":7,"f":0.35},{"b":41.5,"et":2,"i":6,"f":0.55},{"b":41.5,"et":4,"i":7,"f":0.45},{"b":41.75002268,"et":2,"i":7,"f":0.35},{"b":42.0,"et":1,"i":7,"f":0.75},{"b":42.0,"et":2,"i":5,"f":0.55},{"b":42.0,"et":3,"i":5,"f":0.55},{"b":42.0,"et":4,"i":5,"f":0.6},{"b":42.24997732,"et":2,"i":7,"f":0.35},{"b":42.5,"et":2,"i":6,"f":0.55},{"b":42.5,"et":4,"i":7,"f":0.45},{"b":42.75002268,"et":2,"i":7,"f":0.35},{"b":43.0,"et":2,"i":6,"f":0.55},{"b":43.0,"et":4,"i":7,"f":0.45},{"b":43.24997732,"et":2,"i":7,"f":0.35},{"b":43.5,"et":2,"i":6,"f":0.55},{"b":43.5,"et":4,"i":7,"f":0.45},{"b":43.75002268,"et":2,"i":7,"f":0.35},{"b":44.0,"et":0,"i":1,"f":0.65},{"b":44.0,"et":1,"i":2,"f":1.0},{"b":44.0,"et":4,"i":1,"f":0.4},{"b":44.5,"et":2,"i":3,"f":0.32},{"b":45.0,"et":2,"i":3,"f":0.32},{"b":45.5,"et":2,"i":3,"f":0.32},{"b":46.0,"et":1,"i":3,"f":0.4},{"b":46.0,"et":4,"i":1,"f":0.4},{"b":46.5,"et":2,"i":3,"f":0.32},{"b":47.0,"et":2,"i":3,"f":0.32},{"b":47.5,"et":2,"i":3,"f":0.32},{"b":48.0,"et":0,"i":5,"f":0.95},{"b":48.0,"et":1,"i":6,"f":1.0},{"b":48.0,"et":2,"i":6,"f":0.85},{"b":48.0,"et":3,"i":6,"f":0.85},{"b":48.0,"et":4,"i":6,"f":0.95},{"b":48.24997732,"et":2,"i":7,"f":0.35},{"b":48.5,"et":2,"i":6,"f":0.55},{"b":48.5,"et":4,"i":7,"f":0.45},{"b":48.75002268,"et":2,"i":7,"f":0.35},{"b":49.0,"et":2,"i":6,"f":0.55},{"b":49.0,"et":4,"i":7,"f":0.45},{"b":49.24997732,"et":2,"i":7,"f":0.35},{"b":49.5,"et":2,"i":6,"f":0.55},{"b":49.5,"et":4,"i":7,"f":0.45},{"b":49.75002268,"et":2,"i":7,"f":0.35},{"b":50.0,"et":1,"i":7,"f":0.75},{"b":50.0,"et":2,"i":5,"f":0.55},{"b":50.0,"et":3,"i":5,"f":0.55},{"b":50.0,"et":4,"i":5,"f":0.6},{"b":50.24997732,"et":2,"i":7,"f":0.35},{"b":50.5,"et":2,"i":6,"f":0.55},{"b":50.5,"et":4,"i":7,"f":0.45},{"b":50.75002268,"et":2,"i":7,"f":0.35},{"b":51.0,"et":2,"i":6,"f":0.55},{"b":51.0,"et":4,"i":7,"f":0.45},{"b":51.24997732,"et":2,"i":7,"f":0.35},{"b":51.5,"et":2,"i":6,"f":0.55},{"b":51.5,"et":4,"i":7,"f":0.45},{"b":51.75002268,"et":2,"i":7,"f":0.35},{"b":52.0,"et":0,"i":1,"f":0.95},{"b":52.0,"et":1,"i":2,"f":1.0},{"b":52.0,"et":2,"i":2,"f":0.85},{"b":52.0,"et":3,"i":2,"f":0.85},{"b":52.0,"et":4,"i":2,"f":0.95},{"b":52.24997732,"et":2,"i":3,"f":0.35},{"b":52.5,"et":2,"i":2,"f":0.55},{"b":52.5,"et":4,"i":3,"f":0.45},{"b":52.75002268,"et":2,"i":3,"f":0.35},{"b":53.0,"et":2,"i":2,"f":0.55},{"b":53.0,"et":4,"i":3,"f":0.45},{"b":53.24997732,"et":2,"i":3,"f":0.35},{"b":53.5,"et":2,"i":2,"f":0.55},{"b":53.5,"et":4,"i":3,"f":0.45},{"b":53.75002268,"et":2,"i":3,"f":0.35},{"b":54.0,"et":1,"i":3,"f":0.75},{"b":54.0,"et":2,"i":1,"f":0.55},{"b":54.0,"et":3,"i":1,"f":0.55},{"b":54.0,"et":4,"i":1,"f":0.6},{"b":54.24997732,"et":2,"i":3,"f":0.35},{"b":54.5,"et":2,"i":2,"f":0.55},{"b":54.5,"et":4,"i":3,"f":0.45},{"b":54.75002268,"et":2,"i":3,"f":0.35},{"b":55.0,"et":2,"i":2,"f":0.55},{"b":55.0,"et":4,"i":3,"f":0.45},{"b":55.24997732,"et":2,"i":3,"f":0.35},{"b":55.5,"et":2,"i":2,"f":0.55},{"b":55.5,"et":4,"i":3,"f":0.45},{"b":55.75002268,"et":2,"i":3,"f":0.35},{"b":56.0,"et":0,"i":5,"f":0.95},{"b":56.0,"et":1,"i":6,"f":1.0},{"b":56.0,"et":2,"i":6,"f":0.85},{"b":56.0,"et":3,"i":6,"f":0.85},{"b":56.0,"et":4,"i":6,"f":0.95},{"b":56.24997732,"et":2,"i":7,"f":0.35},{"b":56.5,"et":2,"i":6,"f":0.55},{"b":56.5,"et":4,"i":7,"f":0.45},{"b":56.75002268,"et":2,"i":7,"f":0.35},{"b":57.0,"et":2,"i":6,"f":0.55},{"b":57.0,"et":4,"i":7,"f":0.45},{"b":57.24997732,"et":2,"i":7,"f":0.35},{"b":57.5,"et":2,"i":6,"f":0.55},{"b":57.5,"et":4,"i":7,"f":0.45},{"b":57.75002268,"et":2,"i":7,"f":0.35},{"b":58.0,"et":1,"i":7,"f":0.75},{"b":58.0,"et":2,"i":5,"f":0.55},{"b":58.0,"et":3,"i":5,"f":0.55},{"b":58.0,"et":4,"i":5,"f":0.6},{"b":58.24997732,"et":2,"i":7,"f":0.35},{"b":58.5,"et":2,"i":6,"f":0.55},{"b":58.5,"et":4,"i":7,"f":0.45},{"b":58.75002268,"et":2,"i":7,"f":0.35},{"b":59.0,"et":2,"i":6,"f":0.55},{"b":59.0,"et":4,"i":7,"f":0.45},{"b":59.24997732,"et":2,"i":7,"f":0.35},{"b":59.5,"et":2,"i":6,"f":0.55},{"b":59.5,"et":4,"i":7,"f":0.45},{"b":59.75002268,"et":2,"i":7,"f":0.35},{"b":60.0,"et":0,"i":1,"f":0.95},{"b":60.0,"et":1,"i":2,"f":1.0},{"b":60.0,"et":2,"i":2,"f":0.85},{"b":60.0,"et":3,"i":2,"f":0.85},{"b":60.0,"et":4,"i":2,"f":0.95}],"colorBoostBeatmapEvents":[{"b":4.0,"o":true},{"b":8.0,"o":false},{"b":24.0,"o":true},{"b":44.0,"o":false},{"b":48.0,"o":true}],"lightColorEventBoxGroups":[{"b":0.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":24.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":44.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":48.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]}],"lightRotationEventBoxGroups":[{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]}],"lightTranslationEventBoxGroups":[{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]}],"vfxEventBoxGroups":[],"basicEventTypesWithKeywords":{},"useNormalEventsAsCompatibleEvents":true,"customData":{"_beatforgeTiming":{"schemaVersion":1,"clock":"constant-bpm-sample-projection","sampleRate":44100,"bpm":120.0}}},"Hard":{"version":"3.3.0","bpmEvents":[],"rotationEvents":[],"colorNotes":[{"b":4.0,"x":3,"y":1,"c":1,"d":7,"a":0},{"b":4.5,"x":1,"y":0,"c":0,"d":7,"a":0},{"b":5.0,"x":3,"y":0,"c":1,"d":0,"a":0},{"b":8.0,"x":1,"y":0,"c":0,"d":2,"a":0},{"b":8.5,"x":0,"y":0,"c":0,"d":3,"a":0},{"b":12.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":12.5,"x":2,"y":0,"c":1,"d":0,"a":0},{"b":16.0,"x":1,"y":0,"c":0,"d":0,"a":0},{"b":16.5,"x":2,"y":1,"c":0,"d":1,"a":0},{"b":20.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":20.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":24.0,"x":0,"y":0,"c":0,"d":3,"a":0},{"b":24.5,"x":1,"y":0,"c":0,"d":2,"a":0},{"b":25.0,"x":3,"y":1,"c":1,"d":3,"a":0},{"b":27.0,"x":3,"y":1,"c":1,"d":2,"a":0},{"b":27.5,"x":2,"y":1,"c":1,"d":3,"a":0},{"b":28.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":29.0,"x":2,"y":1,"c":1,"d":0,"a":0},{"b":29.5,"x":1,"y":2,"c":1,"d":1,"a":0},{"b":31.0,"x":1,"y":1,"c":0,"d":1,"a":0},{"b":31.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":32.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":33.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":33.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":35.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":35.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":36.0,"x":0,"y":1,"c":0,"d":1,"a":0},{"b":37.0,"x":2,"y":1,"c":1,"d":1,"a":0},{"b":37.5,"x":1,"y":0,"c":1,"d":0,"a":0},{"b":39.0,"x":1,"y":1,"c":0,"d":0,"a":0},{"b":39.5,"x":2,"y":2,"c":0,"d":1,"a":0},{"b":44.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":44.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":48.0,"x":0,"y":0,"c":0,"d":3,"a":0},{"b":48.5,"x":1,"y":0,"c":0,"d":2,"a":0},{"b":49.0,"x":3,"y":1,"c":1,"d":3,"a":0},{"b":51.0,"x":1,"y":1,"c":0,"d":2,"a":0},{"b":51.5,"x":0,"y":1,"c":0,"d":3,"a":0},{"b":52.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":53.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":53.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":55.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":55.5,"x":2,"y":0,"c":1,"d":0,"a":0},{"b":56.0,"x":0,"y":1,"c":0,"d":1,"a":0},{"b":57.0,"x":2,"y":1,"c":1,"d":0,"a":0},{"b":57.5,"x":1,"y":2,"c":1,"d":1,"a":0},{"b":59.0,"x":1,"y":1,"c":0,"d":0,"a":0},{"b":59.5,"x":2,"y":2,"c":0,"d":1,"a":0}],"bombNotes":[{"b":5.0,"x":0,"y":2},{"b":30.0,"x":3,"y":2}],"obstacles":[{"b":44.5,"d":1.0,"x":0,"y":0,"w":2,"h":5}],"sliders":[{"c":1,"b":4.0,"x":3,"y":1,"d":7,"mu":0.7,"tb":5.0,"tx":3,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":8.0,"x":1,"y":0,"d":2,"mu":0.7,"tb":8.5,"tx":0,"ty":0,"tc":3,"tmu":0.7,"m":0},{"c":1,"b":12.0,"x":3,"y":1,"d":1,"mu":0.7,"tb":12.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":16.0,"x":1,"y":0,"d":0,"mu":0.7,"tb":16.5,"tx":2,"ty":1,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":20.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":20.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":24.0,"x":0,"y":0,"d":3,"mu":0.7,"tb":24.5,"tx":1,"ty":0,"tc":2,"tmu":0.7,"m":0},{"c":1,"b":27.0,"x":3,"y":1,"d":2,"mu":0.7,"tb":27.5,"tx":2,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":1,"b":29.0,"x":2,"y":1,"d":0,"mu":0.7,"tb":29.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":31.0,"x":1,"y":1,"d":1,"mu":0.7,"tb":31.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":33.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":33.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":35.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":35.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":37.0,"x":2,"y":1,"d":1,"mu":0.7,"tb":37.5,"tx":1,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":39.0,"x":1,"y":1,"d":0,"mu":0.7,"tb":39.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":44.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":44.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":48.0,"x":0,"y":0,"d":3,"mu":0.7,"tb":48.5,"tx":1,"ty":0,"tc":2,"tmu":0.7,"m":0},{"c":0,"b":51.0,"x":1,"y":1,"d":2,"mu":0.7,"tb":51.5,"tx":0,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":0,"b":53.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":53.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":55.0,"x":3,"y":1,"d":1,"mu":0.7,"tb":55.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":57.0,"x":2,"y":1,"d":0,"mu":0.7,"tb":57.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":59.0,"x":1,"y":1,"d":0,"mu":0.7,"tb":59.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0}],"burstSliders":[],"waypoints":[],"basicBeatmapEvents":[{"b":0.0,"et":0,"i":5,"f":0.45},{"b":0.0,"et":1,"i":7,"f":0.35},{"b":4.0,"et":0,"i":1,"f":0.45},{"b":4.0,"et":1,"i":6,"f":1.0},{"b":8.0,"et":0,"i":5,"f":0.65},{"b":8.0,"et":1,"i":2,"f":1.0},{"b":8.0,"et":4,"i":5,"f":0.4},{"b":8.5,"et":2,"i":7,"f":0.32},{"b":9.0,"et":2,"i":7,"f":0.32},{"b":9.5,"et":2,"i":7,"f":0.32},{"b":10.0,"et":1,"i":7,"f":0.4},{"b":10.0,"et":4,"i":5,"f":0.4},{"b":10.5,"et":2,"i":7,"f":0.32},{"b":11.0,"et":2,"i":7,"f":0.32},{"b":11.5,"et":2,"i":7,"f":0.32},{"b":12.0,"et":0,"i":1,"f":0.65},{"b":12.0,"et":1,"i":3,"f":0.55},{"b":12.0,"et":4,"i":1,"f":0.4},{"b":12.5,"et":2,"i":3,"f":0.32},{"b":13.0,"et":2,"i":3,"f":0.32},{"b":13.5,"et":2,"i":3,"f":0.32},{"b":14.0,"et":1,"i":3,"f":0.4},{"b":14.0,"et":4,"i":1,"f":0.4},{"b":14.5,"et":2,"i":3,"f":0.32},{"b":15.0,"et":2,"i":3,"f":0.32},{"b":15.5,"et":2,"i":3,"f":0.32},{"b":16.0,"et":0,"i":5,"f":0.65},{"b":16.0,"et":1,"i":7,"f":0.55},{"b":16.0,"et":4,"i":5,"f":0.4},{"b":16.5,"et":2,"i":7,"f":0.32},{"b":17.0,"et":2,"i":7,"f":0.32},{"b":17.5,"et":2,"i":7,"f":0.32},{"b":18.0,"et":1,"i":7,"f":0.4},{"b":18.0,"et":4,"i":5,"f":0.4},{"b":18.5,"et":2,"i":7,"f":0.32},{"b":19.0,"et":2,"i":7,"f":0.32},{"b":19.5,"et":2,"i":7,"f":0.32},{"b":20.0,"et":0,"i":1,"f":0.65},{"b":20.0,"et":1,"i":3,"f":0.55},{"b":20.0,"et":4,"i":1,"f":0.4},{"b":20.5,"et":2,"i":3,"f":0.32},{"b":21.0,"et":2,"i":3,"f":0.32},{"b":21.5,"et":2,"i":3,"f":0.32},{"b":22.0,"et":1,"i":3,"f":0.4},{"b":22.0,"et":4,"i":1,"f":0.4},{"b":22.5,"et":2,"i":3,"f":0.32},{"b":23.0,"et":2,"i":3,"f":0.32},{"b":23.5,"et":2,"i":3,"f":0.32},{"b":24.0,"et":0,"i":5,"f":0.95},{"b":24.0,"et":1,"i":6,"f":1.0},{"b":24.0,"et":2,"i":6,"f":0.85},{"b":24.0,"et":3,"i":6,"f":0.85},{"b":24.0,"et":4,"i":6,"f":0.95},{"b":24.24997732,"et":2,"i":7,"f":0.35},{"b":24.5,"et":2,"i":6,"f":0.55},{"b":24.5,"et":4,"i":7,"f":0.45},{"b":24.75002268,"et":2,"i":7,"f":0.35},{"b":25.0,"et":2,"i":6,"f":0.55},{"b":25.0,"et":4,"i":7,"f":0.45},{"b":25.24997732,"et":2,"i":7,"f":0.35},{"b":25.5,"et":2,"i":6,"f":0.55},{"b":25.5,"et":4,"i":7,"f":0.45},{"b":25.75002268,"et":2,"i":7,"f":0.35},{"b":26.0,"et":1,"i":7,"f":0.75},{"b":26.0,"et":2,"i":5,"f":0.55},{"b":26.0,"et":3,"i":5,"f":0.55},{"b":26.0,"et":4,"i":5,"f":0.6},{"b":26.24997732,"et":2,"i":7,"f":0.35},{"b":26.5,"et":2,"i":6,"f":0.55},{"b":26.5,"et":4,"i":7,"f":0.45},{"b":26.75002268,"et":2,"i":7,"f":0.35},{"b":27.0,"et":2,"i":6,"f":0.55},{"b":27.0,"et":4,"i":7,"f":0.45},{"b":27.24997732,"et":2,"i":7,"f":0.35},{"b":27.5,"et":2,"i":6,"f":0.55},{"b":27.5,"et":4,"i":7,"f":0.45},{"b":27.75002268,"et":2,"i":7,"f":0.35},{"b":28.0,"et":0,"i":1,"f":0.95},{"b":28.0,"et":1,"i":2,"f":1.0},{"b":28.0,"et":2,"i":2,"f":0.85},{"b":28.0,"et":3,"i":2,"f":0.85},{"b":28.0,"et":4,"i":2,"f":0.95},{"b":28.24997732,"et":2,"i":3,"f":0.35},{"b":28.5,"et":2,"i":2,"f":0.55},{"b":28.5,"et":4,"i":3,"f":0.45},{"b":28.75002268,"et":2,"i":3,"f":0.35},{"b":29.0,"et":2,"i":2,"f":0.55},{"b":29.0,"et":4,"i":3,"f":0.45},{"b":29.24997732,"et":2,"i":3,"f":0.35},{"b":29.5,"et":2,"i":2,"f":0.55},{"b":29.5,"et":4,"i":3,"f":0.45},{"b":29.75002268,"et":2,"i":3,"f":0.35},{"b":30.0,"et":1,"i":3,"f":0.75},{"b":30.0,"et":2,"i":1,"f":0.55},{"b":30.0,"et":3,"i":1,"f":0.55},{"b":30.0,"et":4,"i":1,"f":0.6},{"b":30.24997732,"et":2,"i":3,"f":0.35},{"b":30.5,"et":2,"i":2,"f":0.55},{"b":30.5,"et":4,"i":3,"f":0.45},{"b":30.75002268,"et":2,"i":3,"f":0.35},{"b":31.0,"et":2,"i":2,"f":0.55},{"b":31.0,"et":4,"i":3,"f":0.45},{"b":31.24997732,"et":2,"i":3,"f":0.35},{"b":31.5,"et":2,"i":2,"f":0.55},{"b":31.5,"et":4,"i":3,"f":0.45},{"b":31.75002268,"et":2,"i":3,"f":0.35},{"b":32.0,"et":0,"i":5,"f":0.95},{"b":32.0,"et":1,"i":6,"f":1.0},{"b":32.0,"et":2,"i":6,"f":0.85},{"b":32.0,"et":3,"i":6,"f":0.85},{"b":32.0,"et":4,"i":6,"f":0.95},{"b":32.24997732,"et":2,"i":7,"f":0.35},{"b":32.5,"et":2,"i":6,"f":0.55},{"b":32.5,"et":4,"i":7,"f":0.45},{"b":32.75002268,"et":2,"i":7,"f":0.35},{"b":33.0,"et":2,"i":6,"f":0.55},{"b":33.0,"et":4,"i":7,"f":0.45},{"b":33.24997732,"et":2,"i":7,"f":0.35},{"b":33.5,"et":2,"i":6,"f":0.55},{"b":33.5,"et":4,"i":7,"f":0.45},{"b":33.75002268,"et":2,"i":7,"f":0.35},{"b":34.0,"et":1,"i":7,"f":0.75},{"b":34.0,"et":2,"i":5,"f":0.55},{"b":34.0,"et":3,"i":5,"f":0.55},{"b":34.0,"et":4,"i":5,"f":0.6},{"b":34.24997732,"et":2,"i":7,"f":0.35},{"b":34.5,"et":2,"i":6,"f":0.55},{"b":34.5,"et":4,"i":7,"f":0.45},{"b":34.75002268,"et":2,"i":7,"f":0.35},{"b":35.0,"et":2,"i":6,"f":0.55},{"b":35.0,"et":4,"i":7,"f":0.45},{"b":35.24997732,"et":2,"i":7,"f":0.35},{"b":35.5,"et":2,"i":6,"f":0.55},{"b":35.5,"et":4,"i":7,"f":0.45},{"b":35.75002268,"et":2,"i":7,"f":0.35},{"b":36.0,"et":0,"i":1,"f":0.95},{"b":36.0,"et":1,"i":2,"f":1.0},{"b":36.0,"et":2,"i":2,"f":0.85},{"b":36.0,"et":3,"i":2,"f":0.85},{"b":36.0,"et":4,"i":2,"f":0.95},{"b":36.24997732,"et":2,"i":3,"f":0.35},{"b":36.5,"et":2,"i":2,"f":0.55},{"b":36.5,"et":4,"i":3,"f":0.45},{"b":36.75002268,"et":2,"i":3,"f":0.35},{"b":37.0,"et":2,"i":2,"f":0.55},{"b":37.0,"et":4,"i":3,"f":0.45},{"b":37.24997732,"et":2,"i":3,"f":0.35},{"b":37.5,"et":2,"i":2,"f":0.55},{"b":37.5,"et":4,"i":3,"f":0.45},{"b":37.75002268,"et":2,"i":3,"f":0.35},{"b":38.0,"et":1,"i":3,"f":0.75},{"b":38.0,"et":2,"i":1,"f":0.55},{"b":38.0,"et":3,"i":1,"f":0.55},{"b":38.0,"et":4,"i":1,"f":0.6},{"b":38.24997732,"et":2,"i":3,"f":0.35},{"b":38.5,"et":2,"i":2,"f":0.55},{"b":38.5,"et":4,"i":3,"f":0.45},{"b":38.75002268,"et":2,"i":3,"f":0.35},{"b":39.0,"et":2,"i":2,"f":0.55},{"b":39.0,"et":4,"i":3,"f":0.45},{"b":39.24997732,"et":2,"i":3,"f":0.35},{"b":39.5,"et":2,"i":2,"f":0.55},{"b":39.5,"et":4,"i":3,"f":0.45},{"b":39.75002268,"et":2,"i":3,"f":0.35},{"b":40.0,"et":0,"i":5,"f":0.95},{"b":40.0,"et":1,"i":6,"f":1.0},{"b":40.0,"et":2,"i":6,"f":0.85},{"b":40.0,"et":3,"i":6,"f":0.85},{"b":40.0,"et":4,"i":6,"f":0.95},{"b":40.24997732,"et":2,"i":7,"f":0.35},{"b":40.5,"et":2,"i":6,"f":0.55},{"b":40.5,"et":4,"i":7,"f":0.45},{"b":40.75002268,"et":2,"i":7,"f":0.35},{"b":41.0,"et":2,"i":6,"f":0.55},{"b":41.0,"et":4,"i":7,"f":0.45},{"b":41.24997732,"et":2,"i":7,"f":0.35},{"b":41.5,"et":2,"i":6,"f":0.55},{"b":41.5,"et":4,"i":7,"f":0.45},{"b":41.75002268,"et":2,"i":7,"f":0.35},{"b":42.0,"et":1,"i":7,"f":0.75},{"b":42.0,"et":2,"i":5,"f":0.55},{"b":42.0,"et":3,"i":5,"f":0.55},{"b":42.0,"et":4,"i":5,"f":0.6},{"b":42.24997732,"et":2,"i":7,"f":0.35},{"b":42.5,"et":2,"i":6,"f":0.55},{"b":42.5,"et":4,"i":7,"f":0.45},{"b":42.75002268,"et":2,"i":7,"f":0.35},{"b":43.0,"et":2,"i":6,"f":0.55},{"b":43.0,"et":4,"i":7,"f":0.45},{"b":43.24997732,"et":2,"i":7,"f":0.35},{"b":43.5,"et":2,"i":6,"f":0.55},{"b":43.5,"et":4,"i":7,"f":0.45},{"b":43.75002268,"et":2,"i":7,"f":0.35},{"b":44.0,"et":0,"i":1,"f":0.65},{"b":44.0,"et":1,"i":2,"f":1.0},{"b":44.0,"et":4,"i":1,"f":0.4},{"b":44.5,"et":2,"i":3,"f":0.32},{"b":45.0,"et":2,"i":3,"f":0.32},{"b":45.5,"et":2,"i":3,"f":0.32},{"b":46.0,"et":1,"i":3,"f":0.4},{"b":46.0,"et":4,"i":1,"f":0.4},{"b":46.5,"et":2,"i":3,"f":0.32},{"b":47.0,"et":2,"i":3,"f":0.32},{"b":47.5,"et":2,"i":3,"f":0.32},{"b":48.0,"et":0,"i":5,"f":0.95},{"b":48.0,"et":1,"i":6,"f":1.0},{"b":48.0,"et":2,"i":6,"f":0.85},{"b":48.0,"et":3,"i":6,"f":0.85},{"b":48.0,"et":4,"i":6,"f":0.95},{"b":48.24997732,"et":2,"i":7,"f":0.35},{"b":48.5,"et":2,"i":6,"f":0.55},{"b":48.5,"et":4,"i":7,"f":0.45},{"b":48.75002268,"et":2,"i":7,"f":0.35},{"b":49.0,"et":2,"i":6,"f":0.55},{"b":49.0,"et":4,"i":7,"f":0.45},{"b":49.24997732,"et":2,"i":7,"f":0.35},{"b":49.5,"et":2,"i":6,"f":0.55},{"b":49.5,"et":4,"i":7,"f":0.45},{"b":49.75002268,"et":2,"i":7,"f":0.35},{"b":50.0,"et":1,"i":7,"f":0.75},{"b":50.0,"et":2,"i":5,"f":0.55},{"b":50.0,"et":3,"i":5,"f":0.55},{"b":50.0,"et":4,"i":5,"f":0.6},{"b":50.24997732,"et":2,"i":7,"f":0.35},{"b":50.5,"et":2,"i":6,"f":0.55},{"b":50.5,"et":4,"i":7,"f":0.45},{"b":50.75002268,"et":2,"i":7,"f":0.35},{"b":51.0,"et":2,"i":6,"f":0.55},{"b":51.0,"et":4,"i":7,"f":0.45},{"b":51.24997732,"et":2,"i":7,"f":0.35},{"b":51.5,"et":2,"i":6,"f":0.55},{"b":51.5,"et":4,"i":7,"f":0.45},{"b":51.75002268,"et":2,"i":7,"f":0.35},{"b":52.0,"et":0,"i":1,"f":0.95},{"b":52.0,"et":1,"i":2,"f":1.0},{"b":52.0,"et":2,"i":2,"f":0.85},{"b":52.0,"et":3,"i":2,"f":0.85},{"b":52.0,"et":4,"i":2,"f":0.95},{"b":52.24997732,"et":2,"i":3,"f":0.35},{"b":52.5,"et":2,"i":2,"f":0.55},{"b":52.5,"et":4,"i":3,"f":0.45},{"b":52.75002268,"et":2,"i":3,"f":0.35},{"b":53.0,"et":2,"i":2,"f":0.55},{"b":53.0,"et":4,"i":3,"f":0.45},{"b":53.24997732,"et":2,"i":3,"f":0.35},{"b":53.5,"et":2,"i":2,"f":0.55},{"b":53.5,"et":4,"i":3,"f":0.45},{"b":53.75002268,"et":2,"i":3,"f":0.35},{"b":54.0,"et":1,"i":3,"f":0.75},{"b":54.0,"et":2,"i":1,"f":0.55},{"b":54.0,"et":3,"i":1,"f":0.55},{"b":54.0,"et":4,"i":1,"f":0.6},{"b":54.24997732,"et":2,"i":3,"f":0.35},{"b":54.5,"et":2,"i":2,"f":0.55},{"b":54.5,"et":4,"i":3,"f":0.45},{"b":54.75002268,"et":2,"i":3,"f":0.35},{"b":55.0,"et":2,"i":2,"f":0.55},{"b":55.0,"et":4,"i":3,"f":0.45},{"b":55.24997732,"et":2,"i":3,"f":0.35},{"b":55.5,"et":2,"i":2,"f":0.55},{"b":55.5,"et":4,"i":3,"f":0.45},{"b":55.75002268,"et":2,"i":3,"f":0.35},{"b":56.0,"et":0,"i":5,"f":0.95},{"b":56.0,"et":1,"i":6,"f":1.0},{"b":56.0,"et":2,"i":6,"f":0.85},{"b":56.0,"et":3,"i":6,"f":0.85},{"b":56.0,"et":4,"i":6,"f":0.95},{"b":56.24997732,"et":2,"i":7,"f":0.35},{"b":56.5,"et":2,"i":6,"f":0.55},{"b":56.5,"et":4,"i":7,"f":0.45},{"b":56.75002268,"et":2,"i":7,"f":0.35},{"b":57.0,"et":2,"i":6,"f":0.55},{"b":57.0,"et":4,"i":7,"f":0.45},{"b":57.24997732,"et":2,"i":7,"f":0.35},{"b":57.5,"et":2,"i":6,"f":0.55},{"b":57.5,"et":4,"i":7,"f":0.45},{"b":57.75002268,"et":2,"i":7,"f":0.35},{"b":58.0,"et":1,"i":7,"f":0.75},{"b":58.0,"et":2,"i":5,"f":0.55},{"b":58.0,"et":3,"i":5,"f":0.55},{"b":58.0,"et":4,"i":5,"f":0.6},{"b":58.24997732,"et":2,"i":7,"f":0.35},{"b":58.5,"et":2,"i":6,"f":0.55},{"b":58.5,"et":4,"i":7,"f":0.45},{"b":58.75002268,"et":2,"i":7,"f":0.35},{"b":59.0,"et":2,"i":6,"f":0.55},{"b":59.0,"et":4,"i":7,"f":0.45},{"b":59.24997732,"et":2,"i":7,"f":0.35},{"b":59.5,"et":2,"i":6,"f":0.55},{"b":59.5,"et":4,"i":7,"f":0.45},{"b":59.75002268,"et":2,"i":7,"f":0.35},{"b":60.0,"et":0,"i":1,"f":0.95},{"b":60.0,"et":1,"i":2,"f":1.0},{"b":60.0,"et":2,"i":2,"f":0.85},{"b":60.0,"et":3,"i":2,"f":0.85},{"b":60.0,"et":4,"i":2,"f":0.95}],"colorBoostBeatmapEvents":[{"b":4.0,"o":true},{"b":8.0,"o":false},{"b":24.0,"o":true},{"b":44.0,"o":false},{"b":48.0,"o":true}],"lightColorEventBoxGroups":[{"b":0.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":24.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":44.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":48.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":4.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":8.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":12.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":16.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":20.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":44.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]}],"lightRotationEventBoxGroups":[{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]}],"lightTranslationEventBoxGroups":[{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]}],"vfxEventBoxGroups":[],"basicEventTypesWithKeywords":{},"useNormalEventsAsCompatibleEvents":true,"customData":{"_beatforgeTiming":{"schemaVersion":1,"clock":"constant-bpm-sample-projection","sampleRate":44100,"bpm":120.0}}},"Expert":{"version":"3.3.0","bpmEvents":[],"rotationEvents":[],"colorNotes":[{"b":2.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":3.0,"x":0,"y":2,"c":0,"d":0,"a":0},{"b":4.0,"x":3,"y":1,"c":1,"d":5,"a":0},{"b":4.5,"x":1,"y":0,"c":0,"d":7,"a":0},{"b":5.0,"x":3,"y":2,"c":1,"d":1,"a":0},{"b":6.0,"x":0,"y":1,"c":0,"d":6,"a":0},{"b":6.5,"x":3,"y":0,"c":1,"d":1,"a":0},{"b":7.0,"x":0,"y":0,"c":0,"d":0,"a":0},{"b":8.0,"x":1,"y":2,"c":1,"d":2,"a":0},{"b":8.5,"x":0,"y":2,"c":1,"d":3,"a":0},{"b":9.0,"x":0,"y":1,"c":0,"d":2,"a":0},{"b":9.5,"x":0,"y":1,"c":0,"d":3,"a":0},{"b":10.0,"x":3,"y":1,"c":1,"d":3,"a":0},{"b":10.5,"x":3,"y":1,"c":1,"d":2,"a":0},{"b":11.0,"x":1,"y":1,"c":0,"d":3,"a":0},{"b":11.5,"x":2,"y":1,"c":0,"d":2,"a":0},{"b":12.0,"x":1,"y":2,"c":1,"d":0,"a":0},{"b":12.5,"x":0,"y":2,"c":1,"d":1,"a":0},{"b":13.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":13.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":14.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":14.5,"x":2,"y":0,"c":1,"d":0,"a":0},{"b":15.0,"x":1,"y":1,"c":0,"d":1,"a":0},{"b":15.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":16.0,"x":1,"y":2,"c":1,"d":0,"a":0},{"b":16.5,"x":0,"y":2,"c":1,"d":1,"a":0},{"b":17.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":17.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":18.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":18.5,"x":2,"y":0,"c":1,"d":0,"a":0},{"b":19.0,"x":1,"y":1,"c":0,"d":1,"a":0},{"b":19.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":20.0,"x":1,"y":2,"c":1,"d":0,"a":0},{"b":20.5,"x":0,"y":2,"c":1,"d":1,"a":0},{"b":21.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":21.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":22.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":22.5,"x":2,"y":0,"c":1,"d":0,"a":0},{"b":23.0,"x":1,"y":1,"c":0,"d":1,"a":0},{"b":23.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":24.0,"x":1,"y":2,"c":1,"d":2,"a":0},{"b":24.5,"x":0,"y":2,"c":1,"d":3,"a":0},{"b":25.0,"x":0,"y":1,"c":0,"d":2,"a":0},{"b":25.5,"x":0,"y":1,"c":0,"d":3,"a":0},{"b":26.0,"x":3,"y":1,"c":1,"d":3,"a":0},{"b":26.5,"x":3,"y":1,"c":1,"d":2,"a":0},{"b":27.0,"x":1,"y":1,"c":0,"d":3,"a":0},{"b":27.5,"x":2,"y":1,"c":0,"d":2,"a":0},{"b":28.0,"x":1,"y":2,"c":1,"d":0,"a":0},{"b":28.5,"x":0,"y":2,"c":1,"d":1,"a":0},{"b":29.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":29.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":30.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":30.5,"x":2,"y":0,"c":1,"d":0,"a":0},{"b":31.0,"x":1,"y":1,"c":0,"d":1,"a":0},{"b":31.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":32.0,"x":1,"y":2,"c":1,"d":0,"a":0},{"b":32.5,"x":0,"y":2,"c":1,"d":1,"a":0},{"b":33.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":33.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":34.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":34.5,"x":2,"y":0,"c":1,"d":0,"a":0},{"b":35.0,"x":1,"y":1,"c":0,"d":1,"a":0},{"b":35.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":36.0,"x":1,"y":2,"c":1,"d":0,"a":0},{"b":36.5,"x":0,"y":2,"c":1,"d":1,"a":0},{"b":37.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":37.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":38.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":38.5,"x":2,"y":0,"c":1,"d":0,"a":0},{"b":39.0,"x":1,"y":1,"c":0,"d":1,"a":0},{"b":39.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":40.0,"x":1,"y":2,"c":1,"d":3,"a":0},{"b":40.5,"x":2,"y":2,"c":1,"d":2,"a":0},{"b":41.0,"x":0,"y":1,"c":0,"d":3,"a":0},{"b":41.5,"x":1,"y":1,"c":0,"d":2,"a":0},{"b":42.0,"x":3,"y":1,"c":1,"d":2,"a":0},{"b":42.5,"x":2,"y":1,"c":1,"d":3,"a":0},{"b":43.0,"x":1,"y":1,"c":0,"d":2,"a":0},{"b":43.5,"x":0,"y":1,"c":0,"d":3,"a":0},{"b":44.0,"x":1,"y":2,"c":1,"d":0,"a":0},{"b":44.5,"x":0,"y":2,"c":1,"d":1,"a":0},{"b":45.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":45.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":46.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":46.5,"x":2,"y":0,"c":1,"d":0,"a":0},{"b":47.0,"x":1,"y":1,"c":0,"d":1,"a":0},{"b":47.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":48.0,"x":1,"y":2,"c":1,"d":2,"a":0},{"b":48.5,"x":0,"y":2,"c":1,"d":3,"a":0},{"b":49.0,"x":0,"y":1,"c":0,"d":2,"a":0},{"b":49.5,"x":0,"y":1,"c":0,"d":3,"a":0},{"b":50.0,"x":3,"y":1,"c":1,"d":3,"a":0},{"b":50.5,"x":3,"y":1,"c":1,"d":2,"a":0},{"b":51.0,"x":1,"y":1,"c":0,"d":3,"a":0},{"b":51.5,"x":2,"y":1,"c":0,"d":2,"a":0},{"b":52.0,"x":1,"y":2,"c":1,"d":1,"a":0},{"b":52.5,"x":0,"y":1,"c":1,"d":0,"a":0},{"b":53.0,"x":0,"y":1,"c":0,"d":1,"a":0},{"b":53.5,"x":1,"y":0,"c":0,"d":0,"a":0},{"b":54.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":54.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":55.0,"x":1,"y":1,"c":0,"d":0,"a":0},{"b":55.5,"x":2,"y":2,"c":0,"d":1,"a":0},{"b":56.0,"x":1,"y":2,"c":1,"d":1,"a":0},{"b":56.5,"x":0,"y":1,"c":1,"d":0,"a":0},{"b":57.0,"x":0,"y":1,"c":0,"d":1,"a":0},{"b":57.5,"x":1,"y":0,"c":0,"d":0,"a":0},{"b":58.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":58.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":59.0,"x":1,"y":1,"c":0,"d":0,"a":0},{"b":59.5,"x":2,"y":2,"c":0,"d":1,"a":0}],"bombNotes":[],"obstacles":[{"b":24.5,"d":1.0,"x":2,"y":0,"w":2,"h":5},{"b":48.5,"d":1.0,"x":2,"y":0,"w":2,"h":5}],"sliders":[{"c":1,"b":4.0,"x":3,"y":1,"d":5,"mu":0.7,"tb":5.0,"tx":3,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":6.0,"x":0,"y":1,"d":6,"mu":0.7,"tb":7.0,"tx":0,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":8.0,"x":1,"y":2,"d":2,"mu":0.7,"tb":8.5,"tx":0,"ty":2,"tc":3,"tmu":0.7,"m":0},{"c":0,"b":9.0,"x":0,"y":1,"d":2,"mu":0.7,"tb":9.5,"tx":0,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":1,"b":10.0,"x":3,"y":1,"d":3,"mu":0.7,"tb":10.5,"tx":3,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":0,"b":11.0,"x":1,"y":1,"d":3,"mu":0.7,"tb":11.5,"tx":2,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":1,"b":12.0,"x":1,"y":2,"d":0,"mu":0.7,"tb":12.5,"tx":0,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":13.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":13.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":14.0,"x":3,"y":1,"d":1,"mu":0.7,"tb":14.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":15.0,"x":1,"y":1,"d":1,"mu":0.7,"tb":15.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":16.0,"x":1,"y":2,"d":0,"mu":0.7,"tb":16.5,"tx":0,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":17.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":17.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":18.0,"x":3,"y":1,"d":1,"mu":0.7,"tb":18.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":19.0,"x":1,"y":1,"d":1,"mu":0.7,"tb":19.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":20.0,"x":1,"y":2,"d":0,"mu":0.7,"tb":20.5,"tx":0,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":21.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":21.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":22.0,"x":3,"y":1,"d":1,"mu":0.7,"tb":22.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":23.0,"x":1,"y":1,"d":1,"mu":0.7,"tb":23.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":24.0,"x":1,"y":2,"d":2,"mu":0.7,"tb":24.5,"tx":0,"ty":2,"tc":3,"tmu":0.7,"m":0},{"c":0,"b":25.0,"x":0,"y":1,"d":2,"mu":0.7,"tb":25.5,"tx":0,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":1,"b":26.0,"x":3,"y":1,"d":3,"mu":0.7,"tb":26.5,"tx":3,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":0,"b":27.0,"x":1,"y":1,"d":3,"mu":0.7,"tb":27.5,"tx":2,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":1,"b":28.0,"x":1,"y":2,"d":0,"mu":0.7,"tb":28.5,"tx":0,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":29.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":29.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":30.0,"x":3,"y":1,"d":1,"mu":0.7,"tb":30.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":31.0,"x":1,"y":1,"d":1,"mu":0.7,"tb":31.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":32.0,"x":1,"y":2,"d":0,"mu":0.7,"tb":32.5,"tx":0,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":33.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":33.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":34.0,"x":3,"y":1,"d":1,"mu":0.7,"tb":34.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":35.0,"x":1,"y":1,"d":1,"mu":0.7,"tb":35.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":36.0,"x":1,"y":2,"d":0,"mu":0.7,"tb":36.5,"tx":0,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":37.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":37.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":38.0,"x":3,"y":1,"d":1,"mu":0.7,"tb":38.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":39.0,"x":1,"y":1,"d":1,"mu":0.7,"tb":39.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":40.0,"x":1,"y":2,"d":3,"mu":0.7,"tb":40.5,"tx":2,"ty":2,"tc":2,"tmu":0.7,"m":0},{"c":0,"b":41.0,"x":0,"y":1,"d":3,"mu":0.7,"tb":41.5,"tx":1,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":1,"b":42.0,"x":3,"y":1,"d":2,"mu":0.7,"tb":42.5,"tx":2,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":0,"b":43.0,"x":1,"y":1,"d":2,"mu":0.7,"tb":43.5,"tx":0,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":1,"b":44.0,"x":1,"y":2,"d":0,"mu":0.7,"tb":44.5,"tx":0,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":45.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":45.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":46.0,"x":3,"y":1,"d":1,"mu":0.7,"tb":46.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":47.0,"x":1,"y":1,"d":1,"mu":0.7,"tb":47.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":48.0,"x":1,"y":2,"d":2,"mu":0.7,"tb":48.5,"tx":0,"ty":2,"tc":3,"tmu":0.7,"m":0},{"c":0,"b":49.0,"x":0,"y":1,"d":2,"mu":0.7,"tb":49.5,"tx":0,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":1,"b":50.0,"x":3,"y":1,"d":3,"mu":0.7,"tb":50.5,"tx":3,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":0,"b":51.0,"x":1,"y":1,"d":3,"mu":0.7,"tb":51.5,"tx":2,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":1,"b":52.0,"x":1,"y":2,"d":1,"mu":0.7,"tb":52.5,"tx":0,"ty":1,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":53.0,"x":0,"y":1,"d":1,"mu":0.7,"tb":53.5,"tx":1,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":54.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":54.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":55.0,"x":1,"y":1,"d":0,"mu":0.7,"tb":55.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":56.0,"x":1,"y":2,"d":1,"mu":0.7,"tb":56.5,"tx":0,"ty":1,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":57.0,"x":0,"y":1,"d":1,"mu":0.7,"tb":57.5,"tx":1,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":58.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":58.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":59.0,"x":1,"y":1,"d":0,"mu":0.7,"tb":59.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0}],"burstSliders":[],"waypoints":[],"basicBeatmapEvents":[{"b":0.0,"et":0,"i":5,"f":0.45},{"b":0.0,"et":1,"i":7,"f":0.35},{"b":2.0,"et":1,"i":6,"f":1.0},{"b":4.0,"et":0,"i":1,"f":0.45},{"b":4.0,"et":1,"i":3,"f":0.35},{"b":8.0,"et":0,"i":5,"f":0.65},{"b":8.0,"et":1,"i":2,"f":1.0},{"b":8.0,"et":4,"i":5,"f":0.4},{"b":8.5,"et":2,"i":7,"f":0.32},{"b":9.0,"et":2,"i":7,"f":0.32},{"b":9.5,"et":2,"i":7,"f":0.32},{"b":10.0,"et":1,"i":7,"f":0.4},{"b":10.0,"et":4,"i":5,"f":0.4},{"b":10.5,"et":2,"i":7,"f":0.32},{"b":11.0,"et":2,"i":7,"f":0.32},{"b":11.5,"et":2,"i":7,"f":0.32},{"b":12.0,"et":0,"i":1,"f":0.65},{"b":12.0,"et":1,"i":3,"f":0.55},{"b":12.0,"et":4,"i":1,"f":0.4},{"b":12.5,"et":2,"i":3,"f":0.32},{"b":13.0,"et":2,"i":3,"f":0.32},{"b":13.5,"et":2,"i":3,"f":0.32},{"b":14.0,"et":1,"i":3,"f":0.4},{"b":14.0,"et":4,"i":1,"f":0.4},{"b":14.5,"et":2,"i":3,"f":0.32},{"b":15.0,"et":2,"i":3,"f":0.32},{"b":15.5,"et":2,"i":3,"f":0.32},{"b":16.0,"et":0,"i":5,"f":0.65},{"b":16.0,"et":1,"i":7,"f":0.55},{"b":16.0,"et":4,"i":5,"f":0.4},{"b":16.5,"et":2,"i":7,"f":0.32},{"b":17.0,"et":2,"i":7,"f":0.32},{"b":17.5,"et":2,"i":7,"f":0.32},{"b":18.0,"et":1,"i":7,"f":0.4},{"b":18.0,"et":4,"i":5,"f":0.4},{"b":18.5,"et":2,"i":7,"f":0.32},{"b":19.0,"et":2,"i":7,"f":0.32},{"b":19.5,"et":2,"i":7,"f":0.32},{"b":20.0,"et":0,"i":1,"f":0.65},{"b":20.0,"et":1,"i":3,"f":0.55},{"b":20.0,"et":4,"i":1,"f":0.4},{"b":20.5,"et":2,"i":3,"f":0.32},{"b":21.0,"et":2,"i":3,"f":0.32},{"b":21.5,"et":2,"i":3,"f":0.32},{"b":22.0,"et":1,"i":3,"f":0.4},{"b":22.0,"et":4,"i":1,"f":0.4},{"b":22.5,"et":2,"i":3,"f":0.32},{"b":23.0,"et":2,"i":3,"f":0.32},{"b":23.5,"et":2,"i":3,"f":0.32},{"b":24.0,"et":0,"i":5,"f":0.95},{"b":24.0,"et":1,"i":6,"f":1.0},{"b":24.0,"et":2,"i":6,"f":0.85},{"b":24.0,"et":3,"i":6,"f":0.85},{"b":24.0,"et":4,"i":6,"f":0.95},{"b":24.24997732,"et":2,"i":7,"f":0.35},{"b":24.5,"et":2,"i":6,"f":0.55},{"b":24.5,"et":4,"i":7,"f":0.45},{"b":24.75002268,"et":2,"i":7,"f":0.35},{"b":25.0,"et":2,"i":6,"f":0.55},{"b":25.0,"et":4,"i":7,"f":0.45},{"b":25.24997732,"et":2,"i":7,"f":0.35},{"b":25.5,"et":2,"i":6,"f":0.55},{"b":25.5,"et":4,"i":7,"f":0.45},{"b":25.75002268,"et":2,"i":7,"f":0.35},{"b":26.0,"et":1,"i":7,"f":0.75},{"b":26.0,"et":2,"i":5,"f":0.55},{"b":26.0,"et":3,"i":5,"f":0.55},{"b":26.0,"et":4,"i":5,"f":0.6},{"b":26.24997732,"et":2,"i":7,"f":0.35},{"b":26.5,"et":2,"i":6,"f":0.55},{"b":26.5,"et":4,"i":7,"f":0.45},{"b":26.75002268,"et":2,"i":7,"f":0.35},{"b":27.0,"et":2,"i":6,"f":0.55},{"b":27.0,"et":4,"i":7,"f":0.45},{"b":27.24997732,"et":2,"i":7,"f":0.35},{"b":27.5,"et":2,"i":6,"f":0.55},{"b":27.5,"et":4,"i":7,"f":0.45},{"b":27.75002268,"et":2,"i":7,"f":0.35},{"b":28.0,"et":0,"i":1,"f":0.95},{"b":28.0,"et":1,"i":2,"f":1.0},{"b":28.0,"et":2,"i":2,"f":0.85},{"b":28.0,"et":3,"i":2,"f":0.85},{"b":28.0,"et":4,"i":2,"f":0.95},{"b":28.24997732,"et":2,"i":3,"f":0.35},{"b":28.5,"et":2,"i":2,"f":0.55},{"b":28.5,"et":4,"i":3,"f":0.45},{"b":28.75002268,"et":2,"i":3,"f":0.35},{"b":29.0,"et":2,"i":2,"f":0.55},{"b":29.0,"et":4,"i":3,"f":0.45},{"b":29.24997732,"et":2,"i":3,"f":0.35},{"b":29.5,"et":2,"i":2,"f":0.55},{"b":29.5,"et":4,"i":3,"f":0.45},{"b":29.75002268,"et":2,"i":3,"f":0.35},{"b":30.0,"et":1,"i":3,"f":0.75},{"b":30.0,"et":2,"i":1,"f":0.55},{"b":30.0,"et":3,"i":1,"f":0.55},{"b":30.0,"et":4,"i":1,"f":0.6},{"b":30.24997732,"et":2,"i":3,"f":0.35},{"b":30.5,"et":2,"i":2,"f":0.55},{"b":30.5,"et":4,"i":3,"f":0.45},{"b":30.75002268,"et":2,"i":3,"f":0.35},{"b":31.0,"et":2,"i":2,"f":0.55},{"b":31.0,"et":4,"i":3,"f":0.45},{"b":31.24997732,"et":2,"i":3,"f":0.35},{"b":31.5,"et":2,"i":2,"f":0.55},{"b":31.5,"et":4,"i":3,"f":0.45},{"b":31.75002268,"et":2,"i":3,"f":0.35},{"b":32.0,"et":0,"i":5,"f":0.95},{"b":32.0,"et":1,"i":6,"f":1.0},{"b":32.0,"et":2,"i":6,"f":0.85},{"b":32.0,"et":3,"i":6,"f":0.85},{"b":32.0,"et":4,"i":6,"f":0.95},{"b":32.24997732,"et":2,"i":7,"f":0.35},{"b":32.5,"et":2,"i":6,"f":0.55},{"b":32.5,"et":4,"i":7,"f":0.45},{"b":32.75002268,"et":2,"i":7,"f":0.35},{"b":33.0,"et":2,"i":6,"f":0.55},{"b":33.0,"et":4,"i":7,"f":0.45},{"b":33.24997732,"et":2,"i":7,"f":0.35},{"b":33.5,"et":2,"i":6,"f":0.55},{"b":33.5,"et":4,"i":7,"f":0.45},{"b":33.75002268,"et":2,"i":7,"f":0.35},{"b":34.0,"et":1,"i":7,"f":0.75},{"b":34.0,"et":2,"i":5,"f":0.55},{"b":34.0,"et":3,"i":5,"f":0.55},{"b":34.0,"et":4,"i":5,"f":0.6},{"b":34.24997732,"et":2,"i":7,"f":0.35},{"b":34.5,"et":2,"i":6,"f":0.55},{"b":34.5,"et":4,"i":7,"f":0.45},{"b":34.75002268,"et":2,"i":7,"f":0.35},{"b":35.0,"et":2,"i":6,"f":0.55},{"b":35.0,"et":4,"i":7,"f":0.45},{"b":35.24997732,"et":2,"i":7,"f":0.35},{"b":35.5,"et":2,"i":6,"f":0.55},{"b":35.5,"et":4,"i":7,"f":0.45},{"b":35.75002268,"et":2,"i":7,"f":0.35},{"b":36.0,"et":0,"i":1,"f":0.95},{"b":36.0,"et":1,"i":2,"f":1.0},{"b":36.0,"et":2,"i":2,"f":0.85},{"b":36.0,"et":3,"i":2,"f":0.85},{"b":36.0,"et":4,"i":2,"f":0.95},{"b":36.24997732,"et":2,"i":3,"f":0.35},{"b":36.5,"et":2,"i":2,"f":0.55},{"b":36.5,"et":4,"i":3,"f":0.45},{"b":36.75002268,"et":2,"i":3,"f":0.35},{"b":37.0,"et":2,"i":2,"f":0.55},{"b":37.0,"et":4,"i":3,"f":0.45},{"b":37.24997732,"et":2,"i":3,"f":0.35},{"b":37.5,"et":2,"i":2,"f":0.55},{"b":37.5,"et":4,"i":3,"f":0.45},{"b":37.75002268,"et":2,"i":3,"f":0.35},{"b":38.0,"et":1,"i":3,"f":0.75},{"b":38.0,"et":2,"i":1,"f":0.55},{"b":38.0,"et":3,"i":1,"f":0.55},{"b":38.0,"et":4,"i":1,"f":0.6},{"b":38.24997732,"et":2,"i":3,"f":0.35},{"b":38.5,"et":2,"i":2,"f":0.55},{"b":38.5,"et":4,"i":3,"f":0.45},{"b":38.75002268,"et":2,"i":3,"f":0.35},{"b":39.0,"et":2,"i":2,"f":0.55},{"b":39.0,"et":4,"i":3,"f":0.45},{"b":39.24997732,"et":2,"i":3,"f":0.35},{"b":39.5,"et":2,"i":2,"f":0.55},{"b":39.5,"et":4,"i":3,"f":0.45},{"b":39.75002268,"et":2,"i":3,"f":0.35},{"b":40.0,"et":0,"i":5,"f":0.65},{"b":40.0,"et":1,"i":2,"f":1.0},{"b":40.0,"et":4,"i":5,"f":0.4},{"b":40.5,"et":2,"i":7,"f":0.32},{"b":41.0,"et":2,"i":7,"f":0.32},{"b":41.5,"et":2,"i":7,"f":0.32},{"b":42.0,"et":1,"i":7,"f":0.4},{"b":42.0,"et":4,"i":5,"f":0.4},{"b":42.5,"et":2,"i":7,"f":0.32},{"b":43.0,"et":2,"i":7,"f":0.32},{"b":43.5,"et":2,"i":7,"f":0.32},{"b":44.0,"et":0,"i":1,"f":0.65},{"b":44.0,"et":1,"i":3,"f":0.55},{"b":44.0,"et":4,"i":1,"f":0.4},{"b":44.5,"et":2,"i":3,"f":0.32},{"b":45.0,"et":2,"i":3,"f":0.32},{"b":45.5,"et":2,"i":3,"f":0.32},{"b":46.0,"et":1,"i":3,"f":0.4},{"b":46.0,"et":4,"i":1,"f":0.4},{"b":46.5,"et":2,"i":3,"f":0.32},{"b":47.0,"et":2,"i":3,"f":0.32},{"b":47.5,"et":2,"i":3,"f":0.32},{"b":48.0,"et":0,"i":5,"f":0.95},{"b":48.0,"et":1,"i":6,"f":1.0},{"b":48.0,"et":2,"i":6,"f":0.85},{"b":48.0,"et":3,"i":6,"f":0.85},{"b":48.0,"et":4,"i":6,"f":0.95},{"b":48.24997732,"et":2,"i":7,"f":0.35},{"b":48.5,"et":2,"i":6,"f":0.55},{"b":48.5,"et":4,"i":7,"f":0.45},{"b":48.75002268,"et":2,"i":7,"f":0.35},{"b":49.0,"et":2,"i":6,"f":0.55},{"b":49.0,"et":4,"i":7,"f":0.45},{"b":49.24997732,"et":2,"i":7,"f":0.35},{"b":49.5,"et":2,"i":6,"f":0.55},{"b":49.5,"et":4,"i":7,"f":0.45},{"b":49.75002268,"et":2,"i":7,"f":0.35},{"b":50.0,"et":1,"i":7,"f":0.75},{"b":50.0,"et":2,"i":5,"f":0.55},{"b":50.0,"et":3,"i":5,"f":0.55},{"b":50.0,"et":4,"i":5,"f":0.6},{"b":50.24997732,"et":2,"i":7,"f":0.35},{"b":50.5,"et":2,"i":6,"f":0.55},{"b":50.5,"et":4,"i":7,"f":0.45},{"b":50.75002268,"et":2,"i":7,"f":0.35},{"b":51.0,"et":2,"i":6,"f":0.55},{"b":51.0,"et":4,"i":7,"f":0.45},{"b":51.24997732,"et":2,"i":7,"f":0.35},{"b":51.5,"et":2,"i":6,"f":0.55},{"b":51.5,"et":4,"i":7,"f":0.45},{"b":51.75002268,"et":2,"i":7,"f":0.35},{"b":52.0,"et":0,"i":1,"f":0.95},{"b":52.0,"et":1,"i":2,"f":1.0},{"b":52.0,"et":2,"i":2,"f":0.85},{"b":52.0,"et":3,"i":2,"f":0.85},{"b":52.0,"et":4,"i":2,"f":0.95},{"b":52.24997732,"et":2,"i":3,"f":0.35},{"b":52.5,"et":2,"i":2,"f":0.55},{"b":52.5,"et":4,"i":3,"f":0.45},{"b":52.75002268,"et":2,"i":3,"f":0.35},{"b":53.0,"et":2,"i":2,"f":0.55},{"b":53.0,"et":4,"i":3,"f":0.45},{"b":53.24997732,"et":2,"i":3,"f":0.35},{"b":53.5,"et":2,"i":2,"f":0.55},{"b":53.5,"et":4,"i":3,"f":0.45},{"b":53.75002268,"et":2,"i":3,"f":0.35},{"b":54.0,"et":1,"i":3,"f":0.75},{"b":54.0,"et":2,"i":1,"f":0.55},{"b":54.0,"et":3,"i":1,"f":0.55},{"b":54.0,"et":4,"i":1,"f":0.6},{"b":54.24997732,"et":2,"i":3,"f":0.35},{"b":54.5,"et":2,"i":2,"f":0.55},{"b":54.5,"et":4,"i":3,"f":0.45},{"b":54.75002268,"et":2,"i":3,"f":0.35},{"b":55.0,"et":2,"i":2,"f":0.55},{"b":55.0,"et":4,"i":3,"f":0.45},{"b":55.24997732,"et":2,"i":3,"f":0.35},{"b":55.5,"et":2,"i":2,"f":0.55},{"b":55.5,"et":4,"i":3,"f":0.45},{"b":55.75002268,"et":2,"i":3,"f":0.35},{"b":56.0,"et":0,"i":5,"f":0.95},{"b":56.0,"et":1,"i":6,"f":1.0},{"b":56.0,"et":2,"i":6,"f":0.85},{"b":56.0,"et":3,"i":6,"f":0.85},{"b":56.0,"et":4,"i":6,"f":0.95},{"b":56.24997732,"et":2,"i":7,"f":0.35},{"b":56.5,"et":2,"i":6,"f":0.55},{"b":56.5,"et":4,"i":7,"f":0.45},{"b":56.75002268,"et":2,"i":7,"f":0.35},{"b":57.0,"et":2,"i":6,"f":0.55},{"b":57.0,"et":4,"i":7,"f":0.45},{"b":57.24997732,"et":2,"i":7,"f":0.35},{"b":57.5,"et":2,"i":6,"f":0.55},{"b":57.5,"et":4,"i":7,"f":0.45},{"b":57.75002268,"et":2,"i":7,"f":0.35},{"b":58.0,"et":1,"i":7,"f":0.75},{"b":58.0,"et":2,"i":5,"f":0.55},{"b":58.0,"et":3,"i":5,"f":0.55},{"b":58.0,"et":4,"i":5,"f":0.6},{"b":58.24997732,"et":2,"i":7,"f":0.35},{"b":58.5,"et":2,"i":6,"f":0.55},{"b":58.5,"et":4,"i":7,"f":0.45},{"b":58.75002268,"et":2,"i":7,"f":0.35},{"b":59.0,"et":2,"i":6,"f":0.55},{"b":59.0,"et":4,"i":7,"f":0.45},{"b":59.24997732,"et":2,"i":7,"f":0.35},{"b":59.5,"et":2,"i":6,"f":0.55},{"b":59.5,"et":4,"i":7,"f":0.45},{"b":59.75002268,"et":2,"i":7,"f":0.35},{"b":60.0,"et":0,"i":1,"f":0.95},{"b":60.0,"et":1,"i":2,"f":1.0},{"b":60.0,"et":2,"i":2,"f":0.85},{"b":60.0,"et":3,"i":2,"f":0.85},{"b":60.0,"et":4,"i":2,"f":0.95}],"colorBoostBeatmapEvents":[{"b":2.0,"o":true},{"b":8.0,"o":false},{"b":24.0,"o":true},{"b":40.0,"o":false},{"b":48.0,"o":true}],"lightColorEventBoxGroups":[{"b":0.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":24.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":40.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":40.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":40.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":42.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":42.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":42.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":42.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":48.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":4.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":6.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":8.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":9.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":10.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":11.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":12.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":13.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":14.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":15.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":16.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":17.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":18.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":19.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":20.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":21.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":22.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":23.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":44.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":45.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":46.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":47.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]}],"lightRotationEventBoxGroups":[{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]}],"lightTranslationEventBoxGroups":[{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]}],"vfxEventBoxGroups":[],"basicEventTypesWithKeywords":{},"useNormalEventsAsCompatibleEvents":true,"customData":{"_beatforgeTiming":{"schemaVersion":1,"clock":"constant-bpm-sample-projection","sampleRate":44100,"bpm":120.0}}},"ExpertPlus":{"version":"3.3.0","bpmEvents":[],"rotationEvents":[],"colorNotes":[{"b":2.0,"x":0,"y":1,"c":0,"d":1,"a":0},{"b":3.0,"x":3,"y":2,"c":1,"d":1,"a":0},{"b":4.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":4.5,"x":3,"y":0,"c":1,"d":6,"a":0},{"b":5.0,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":6.0,"x":3,"y":1,"c":1,"d":4,"a":0},{"b":6.5,"x":1,"y":0,"c":0,"d":1,"a":0},{"b":7.0,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":8.0,"x":1,"y":0,"c":0,"d":2,"a":0},{"b":8.5,"x":0,"y":0,"c":0,"d":3,"a":0},{"b":9.0,"x":2,"y":1,"c":1,"d":2,"a":0},{"b":9.5,"x":1,"y":1,"c":1,"d":3,"a":0},{"b":10.0,"x":0,"y":1,"c":0,"d":3,"a":0},{"b":10.5,"x":1,"y":1,"c":0,"d":2,"a":0},{"b":11.0,"x":3,"y":1,"c":1,"d":3,"a":0},{"b":11.5,"x":3,"y":1,"c":1,"d":2,"a":0},{"b":12.0,"x":1,"y":0,"c":0,"d":1,"a":0},{"b":12.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":13.0,"x":2,"y":1,"c":1,"d":1,"a":0},{"b":13.5,"x":1,"y":0,"c":1,"d":0,"a":0},{"b":14.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":14.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":15.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":15.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":16.0,"x":1,"y":0,"c":0,"d":1,"a":0},{"b":16.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":17.0,"x":2,"y":1,"c":1,"d":1,"a":0},{"b":17.5,"x":1,"y":0,"c":1,"d":0,"a":0},{"b":18.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":18.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":19.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":19.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":20.0,"x":1,"y":0,"c":0,"d":1,"a":0},{"b":20.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":21.0,"x":2,"y":1,"c":1,"d":1,"a":0},{"b":21.5,"x":1,"y":0,"c":1,"d":0,"a":0},{"b":22.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":22.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":23.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":23.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":24.0,"x":1,"y":0,"c":0,"d":2,"a":0},{"b":24.5,"x":0,"y":0,"c":0,"d":3,"a":0},{"b":25.0,"x":3,"y":1,"c":1,"d":2,"a":0},{"b":25.5,"x":2,"y":1,"c":1,"d":3,"a":0},{"b":26.0,"x":0,"y":1,"c":0,"d":3,"a":0},{"b":26.5,"x":1,"y":1,"c":0,"d":2,"a":0},{"b":27.0,"x":2,"y":0,"c":1,"d":6,"a":0},{"b":28.0,"x":1,"y":0,"c":0,"d":1,"a":0},{"b":28.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":29.0,"x":3,"y":1,"c":1,"d":1,"a":0},{"b":29.5,"x":2,"y":0,"c":1,"d":0,"a":0},{"b":30.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":30.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":31.0,"x":2,"y":0,"c":1,"d":5,"a":0},{"b":32.0,"x":1,"y":0,"c":0,"d":1,"a":0},{"b":32.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":33.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":33.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":34.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":34.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":35.0,"x":2,"y":0,"c":1,"d":5,"a":0},{"b":36.0,"x":1,"y":0,"c":0,"d":0,"a":0},{"b":36.5,"x":2,"y":1,"c":0,"d":1,"a":0},{"b":37.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":37.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":38.0,"x":0,"y":1,"c":0,"d":1,"a":0},{"b":38.5,"x":1,"y":0,"c":0,"d":0,"a":0},{"b":39.0,"x":2,"y":0,"c":1,"d":5,"a":0},{"b":40.0,"x":1,"y":0,"c":0,"d":2,"a":0},{"b":40.5,"x":0,"y":0,"c":0,"d":3,"a":0},{"b":41.0,"x":2,"y":1,"c":1,"d":2,"a":0},{"b":41.5,"x":1,"y":1,"c":1,"d":3,"a":0},{"b":42.0,"x":0,"y":1,"c":0,"d":3,"a":0},{"b":42.5,"x":1,"y":1,"c":0,"d":2,"a":0},{"b":43.0,"x":3,"y":1,"c":1,"d":3,"a":0},{"b":43.5,"x":3,"y":1,"c":1,"d":2,"a":0},{"b":44.0,"x":1,"y":0,"c":0,"d":1,"a":0},{"b":44.5,"x":2,"y":0,"c":0,"d":0,"a":0},{"b":45.0,"x":2,"y":1,"c":1,"d":1,"a":0},{"b":45.5,"x":1,"y":0,"c":1,"d":0,"a":0},{"b":46.0,"x":0,"y":1,"c":0,"d":0,"a":0},{"b":46.5,"x":1,"y":2,"c":0,"d":1,"a":0},{"b":47.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":47.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":48.0,"x":1,"y":0,"c":0,"d":2,"a":0},{"b":48.5,"x":0,"y":0,"c":0,"d":3,"a":0},{"b":49.0,"x":3,"y":1,"c":1,"d":2,"a":0},{"b":49.5,"x":2,"y":1,"c":1,"d":3,"a":0},{"b":50.0,"x":0,"y":1,"c":0,"d":3,"a":0},{"b":50.5,"x":1,"y":1,"c":0,"d":2,"a":0},{"b":51.0,"x":2,"y":0,"c":1,"d":6,"a":0},{"b":52.0,"x":1,"y":0,"c":0,"d":0,"a":0},{"b":52.5,"x":2,"y":1,"c":0,"d":1,"a":0},{"b":53.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":53.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":54.0,"x":0,"y":1,"c":0,"d":1,"a":0},{"b":54.5,"x":1,"y":0,"c":0,"d":0,"a":0},{"b":55.0,"x":2,"y":0,"c":1,"d":5,"a":0},{"b":56.0,"x":1,"y":0,"c":0,"d":0,"a":0},{"b":56.5,"x":2,"y":1,"c":0,"d":1,"a":0},{"b":57.0,"x":3,"y":1,"c":1,"d":0,"a":0},{"b":57.5,"x":2,"y":2,"c":1,"d":1,"a":0},{"b":58.0,"x":0,"y":1,"c":0,"d":1,"a":0},{"b":58.5,"x":1,"y":0,"c":0,"d":0,"a":0},{"b":59.0,"x":2,"y":0,"c":1,"d":5,"a":0}],"bombNotes":[],"obstacles":[],"sliders":[{"c":0,"b":4.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":5.0,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":6.0,"x":3,"y":1,"d":4,"mu":0.7,"tb":7.0,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":8.0,"x":1,"y":0,"d":2,"mu":0.7,"tb":8.5,"tx":0,"ty":0,"tc":3,"tmu":0.7,"m":0},{"c":1,"b":9.0,"x":2,"y":1,"d":2,"mu":0.7,"tb":9.5,"tx":1,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":0,"b":10.0,"x":0,"y":1,"d":3,"mu":0.7,"tb":10.5,"tx":1,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":1,"b":11.0,"x":3,"y":1,"d":3,"mu":0.7,"tb":11.5,"tx":3,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":0,"b":12.0,"x":1,"y":0,"d":1,"mu":0.7,"tb":12.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":13.0,"x":2,"y":1,"d":1,"mu":0.7,"tb":13.5,"tx":1,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":14.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":14.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":15.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":15.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":16.0,"x":1,"y":0,"d":1,"mu":0.7,"tb":16.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":17.0,"x":2,"y":1,"d":1,"mu":0.7,"tb":17.5,"tx":1,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":18.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":18.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":19.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":19.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":20.0,"x":1,"y":0,"d":1,"mu":0.7,"tb":20.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":21.0,"x":2,"y":1,"d":1,"mu":0.7,"tb":21.5,"tx":1,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":22.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":22.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":23.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":23.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":24.0,"x":1,"y":0,"d":2,"mu":0.7,"tb":24.5,"tx":0,"ty":0,"tc":3,"tmu":0.7,"m":0},{"c":1,"b":25.0,"x":3,"y":1,"d":2,"mu":0.7,"tb":25.5,"tx":2,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":0,"b":26.0,"x":0,"y":1,"d":3,"mu":0.7,"tb":26.5,"tx":1,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":0,"b":28.0,"x":1,"y":0,"d":1,"mu":0.7,"tb":28.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":29.0,"x":3,"y":1,"d":1,"mu":0.7,"tb":29.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":30.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":30.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":32.0,"x":1,"y":0,"d":1,"mu":0.7,"tb":32.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":33.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":33.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":34.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":34.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":36.0,"x":1,"y":0,"d":0,"mu":0.7,"tb":36.5,"tx":2,"ty":1,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":37.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":37.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":38.0,"x":0,"y":1,"d":1,"mu":0.7,"tb":38.5,"tx":1,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":40.0,"x":1,"y":0,"d":2,"mu":0.7,"tb":40.5,"tx":0,"ty":0,"tc":3,"tmu":0.7,"m":0},{"c":1,"b":41.0,"x":2,"y":1,"d":2,"mu":0.7,"tb":41.5,"tx":1,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":0,"b":42.0,"x":0,"y":1,"d":3,"mu":0.7,"tb":42.5,"tx":1,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":1,"b":43.0,"x":3,"y":1,"d":3,"mu":0.7,"tb":43.5,"tx":3,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":0,"b":44.0,"x":1,"y":0,"d":1,"mu":0.7,"tb":44.5,"tx":2,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":1,"b":45.0,"x":2,"y":1,"d":1,"mu":0.7,"tb":45.5,"tx":1,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":46.0,"x":0,"y":1,"d":0,"mu":0.7,"tb":46.5,"tx":1,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":47.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":47.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":48.0,"x":1,"y":0,"d":2,"mu":0.7,"tb":48.5,"tx":0,"ty":0,"tc":3,"tmu":0.7,"m":0},{"c":1,"b":49.0,"x":3,"y":1,"d":2,"mu":0.7,"tb":49.5,"tx":2,"ty":1,"tc":3,"tmu":0.7,"m":0},{"c":0,"b":50.0,"x":0,"y":1,"d":3,"mu":0.7,"tb":50.5,"tx":1,"ty":1,"tc":2,"tmu":0.7,"m":0},{"c":0,"b":52.0,"x":1,"y":0,"d":0,"mu":0.7,"tb":52.5,"tx":2,"ty":1,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":53.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":53.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":54.0,"x":0,"y":1,"d":1,"mu":0.7,"tb":54.5,"tx":1,"ty":0,"tc":0,"tmu":0.7,"m":0},{"c":0,"b":56.0,"x":1,"y":0,"d":0,"mu":0.7,"tb":56.5,"tx":2,"ty":1,"tc":1,"tmu":0.7,"m":0},{"c":1,"b":57.0,"x":3,"y":1,"d":0,"mu":0.7,"tb":57.5,"tx":2,"ty":2,"tc":1,"tmu":0.7,"m":0},{"c":0,"b":58.0,"x":0,"y":1,"d":1,"mu":0.7,"tb":58.5,"tx":1,"ty":0,"tc":0,"tmu":0.7,"m":0}],"burstSliders":[],"waypoints":[],"basicBeatmapEvents":[{"b":0.0,"et":0,"i":5,"f":0.45},{"b":0.0,"et":1,"i":7,"f":0.35},{"b":2.0,"et":1,"i":6,"f":1.0},{"b":4.0,"et":0,"i":1,"f":0.45},{"b":4.0,"et":1,"i":3,"f":0.35},{"b":8.0,"et":0,"i":5,"f":0.65},{"b":8.0,"et":1,"i":2,"f":1.0},{"b":8.0,"et":4,"i":5,"f":0.4},{"b":8.5,"et":2,"i":7,"f":0.32},{"b":9.0,"et":2,"i":7,"f":0.32},{"b":9.5,"et":2,"i":7,"f":0.32},{"b":10.0,"et":1,"i":7,"f":0.4},{"b":10.0,"et":4,"i":5,"f":0.4},{"b":10.5,"et":2,"i":7,"f":0.32},{"b":11.0,"et":2,"i":7,"f":0.32},{"b":11.5,"et":2,"i":7,"f":0.32},{"b":12.0,"et":0,"i":1,"f":0.65},{"b":12.0,"et":1,"i":3,"f":0.55},{"b":12.0,"et":4,"i":1,"f":0.4},{"b":12.5,"et":2,"i":3,"f":0.32},{"b":13.0,"et":2,"i":3,"f":0.32},{"b":13.5,"et":2,"i":3,"f":0.32},{"b":14.0,"et":1,"i":3,"f":0.4},{"b":14.0,"et":4,"i":1,"f":0.4},{"b":14.5,"et":2,"i":3,"f":0.32},{"b":15.0,"et":2,"i":3,"f":0.32},{"b":15.5,"et":2,"i":3,"f":0.32},{"b":16.0,"et":0,"i":5,"f":0.65},{"b":16.0,"et":1,"i":7,"f":0.55},{"b":16.0,"et":4,"i":5,"f":0.4},{"b":16.5,"et":2,"i":7,"f":0.32},{"b":17.0,"et":2,"i":7,"f":0.32},{"b":17.5,"et":2,"i":7,"f":0.32},{"b":18.0,"et":1,"i":7,"f":0.4},{"b":18.0,"et":4,"i":5,"f":0.4},{"b":18.5,"et":2,"i":7,"f":0.32},{"b":19.0,"et":2,"i":7,"f":0.32},{"b":19.5,"et":2,"i":7,"f":0.32},{"b":20.0,"et":0,"i":1,"f":0.65},{"b":20.0,"et":1,"i":3,"f":0.55},{"b":20.0,"et":4,"i":1,"f":0.4},{"b":20.5,"et":2,"i":3,"f":0.32},{"b":21.0,"et":2,"i":3,"f":0.32},{"b":21.5,"et":2,"i":3,"f":0.32},{"b":22.0,"et":1,"i":3,"f":0.4},{"b":22.0,"et":4,"i":1,"f":0.4},{"b":22.5,"et":2,"i":3,"f":0.32},{"b":23.0,"et":2,"i":3,"f":0.32},{"b":23.5,"et":2,"i":3,"f":0.32},{"b":24.0,"et":0,"i":5,"f":0.95},{"b":24.0,"et":1,"i":6,"f":1.0},{"b":24.0,"et":2,"i":6,"f":0.85},{"b":24.0,"et":3,"i":6,"f":0.85},{"b":24.0,"et":4,"i":6,"f":0.95},{"b":24.24997732,"et":2,"i":7,"f":0.35},{"b":24.5,"et":2,"i":6,"f":0.55},{"b":24.5,"et":4,"i":7,"f":0.45},{"b":24.75002268,"et":2,"i":7,"f":0.35},{"b":25.0,"et":2,"i":6,"f":0.55},{"b":25.0,"et":4,"i":7,"f":0.45},{"b":25.24997732,"et":2,"i":7,"f":0.35},{"b":25.5,"et":2,"i":6,"f":0.55},{"b":25.5,"et":4,"i":7,"f":0.45},{"b":25.75002268,"et":2,"i":7,"f":0.35},{"b":26.0,"et":1,"i":7,"f":0.75},{"b":26.0,"et":2,"i":5,"f":0.55},{"b":26.0,"et":3,"i":5,"f":0.55},{"b":26.0,"et":4,"i":5,"f":0.6},{"b":26.24997732,"et":2,"i":7,"f":0.35},{"b":26.5,"et":2,"i":6,"f":0.55},{"b":26.5,"et":4,"i":7,"f":0.45},{"b":26.75002268,"et":2,"i":7,"f":0.35},{"b":27.0,"et":2,"i":6,"f":0.55},{"b":27.0,"et":4,"i":7,"f":0.45},{"b":27.24997732,"et":2,"i":7,"f":0.35},{"b":27.5,"et":2,"i":6,"f":0.55},{"b":27.5,"et":4,"i":7,"f":0.45},{"b":27.75002268,"et":2,"i":7,"f":0.35},{"b":28.0,"et":0,"i":1,"f":0.95},{"b":28.0,"et":1,"i":2,"f":1.0},{"b":28.0,"et":2,"i":2,"f":0.85},{"b":28.0,"et":3,"i":2,"f":0.85},{"b":28.0,"et":4,"i":2,"f":0.95},{"b":28.24997732,"et":2,"i":3,"f":0.35},{"b":28.5,"et":2,"i":2,"f":0.55},{"b":28.5,"et":4,"i":3,"f":0.45},{"b":28.75002268,"et":2,"i":3,"f":0.35},{"b":29.0,"et":2,"i":2,"f":0.55},{"b":29.0,"et":4,"i":3,"f":0.45},{"b":29.24997732,"et":2,"i":3,"f":0.35},{"b":29.5,"et":2,"i":2,"f":0.55},{"b":29.5,"et":4,"i":3,"f":0.45},{"b":29.75002268,"et":2,"i":3,"f":0.35},{"b":30.0,"et":1,"i":3,"f":0.75},{"b":30.0,"et":2,"i":1,"f":0.55},{"b":30.0,"et":3,"i":1,"f":0.55},{"b":30.0,"et":4,"i":1,"f":0.6},{"b":30.24997732,"et":2,"i":3,"f":0.35},{"b":30.5,"et":2,"i":2,"f":0.55},{"b":30.5,"et":4,"i":3,"f":0.45},{"b":30.75002268,"et":2,"i":3,"f":0.35},{"b":31.0,"et":2,"i":2,"f":0.55},{"b":31.0,"et":4,"i":3,"f":0.45},{"b":31.24997732,"et":2,"i":3,"f":0.35},{"b":31.5,"et":2,"i":2,"f":0.55},{"b":31.5,"et":4,"i":3,"f":0.45},{"b":31.75002268,"et":2,"i":3,"f":0.35},{"b":32.0,"et":0,"i":5,"f":0.95},{"b":32.0,"et":1,"i":6,"f":1.0},{"b":32.0,"et":2,"i":6,"f":0.85},{"b":32.0,"et":3,"i":6,"f":0.85},{"b":32.0,"et":4,"i":6,"f":0.95},{"b":32.24997732,"et":2,"i":7,"f":0.35},{"b":32.5,"et":2,"i":6,"f":0.55},{"b":32.5,"et":4,"i":7,"f":0.45},{"b":32.75002268,"et":2,"i":7,"f":0.35},{"b":33.0,"et":2,"i":6,"f":0.55},{"b":33.0,"et":4,"i":7,"f":0.45},{"b":33.24997732,"et":2,"i":7,"f":0.35},{"b":33.5,"et":2,"i":6,"f":0.55},{"b":33.5,"et":4,"i":7,"f":0.45},{"b":33.75002268,"et":2,"i":7,"f":0.35},{"b":34.0,"et":1,"i":7,"f":0.75},{"b":34.0,"et":2,"i":5,"f":0.55},{"b":34.0,"et":3,"i":5,"f":0.55},{"b":34.0,"et":4,"i":5,"f":0.6},{"b":34.24997732,"et":2,"i":7,"f":0.35},{"b":34.5,"et":2,"i":6,"f":0.55},{"b":34.5,"et":4,"i":7,"f":0.45},{"b":34.75002268,"et":2,"i":7,"f":0.35},{"b":35.0,"et":2,"i":6,"f":0.55},{"b":35.0,"et":4,"i":7,"f":0.45},{"b":35.24997732,"et":2,"i":7,"f":0.35},{"b":35.5,"et":2,"i":6,"f":0.55},{"b":35.5,"et":4,"i":7,"f":0.45},{"b":35.75002268,"et":2,"i":7,"f":0.35},{"b":36.0,"et":0,"i":1,"f":0.95},{"b":36.0,"et":1,"i":2,"f":1.0},{"b":36.0,"et":2,"i":2,"f":0.85},{"b":36.0,"et":3,"i":2,"f":0.85},{"b":36.0,"et":4,"i":2,"f":0.95},{"b":36.24997732,"et":2,"i":3,"f":0.35},{"b":36.5,"et":2,"i":2,"f":0.55},{"b":36.5,"et":4,"i":3,"f":0.45},{"b":36.75002268,"et":2,"i":3,"f":0.35},{"b":37.0,"et":2,"i":2,"f":0.55},{"b":37.0,"et":4,"i":3,"f":0.45},{"b":37.24997732,"et":2,"i":3,"f":0.35},{"b":37.5,"et":2,"i":2,"f":0.55},{"b":37.5,"et":4,"i":3,"f":0.45},{"b":37.75002268,"et":2,"i":3,"f":0.35},{"b":38.0,"et":1,"i":3,"f":0.75},{"b":38.0,"et":2,"i":1,"f":0.55},{"b":38.0,"et":3,"i":1,"f":0.55},{"b":38.0,"et":4,"i":1,"f":0.6},{"b":38.24997732,"et":2,"i":3,"f":0.35},{"b":38.5,"et":2,"i":2,"f":0.55},{"b":38.5,"et":4,"i":3,"f":0.45},{"b":38.75002268,"et":2,"i":3,"f":0.35},{"b":39.0,"et":2,"i":2,"f":0.55},{"b":39.0,"et":4,"i":3,"f":0.45},{"b":39.24997732,"et":2,"i":3,"f":0.35},{"b":39.5,"et":2,"i":2,"f":0.55},{"b":39.5,"et":4,"i":3,"f":0.45},{"b":39.75002268,"et":2,"i":3,"f":0.35},{"b":40.0,"et":0,"i":5,"f":0.65},{"b":40.0,"et":1,"i":2,"f":1.0},{"b":40.0,"et":4,"i":5,"f":0.4},{"b":40.5,"et":2,"i":7,"f":0.32},{"b":41.0,"et":2,"i":7,"f":0.32},{"b":41.5,"et":2,"i":7,"f":0.32},{"b":42.0,"et":1,"i":7,"f":0.4},{"b":42.0,"et":4,"i":5,"f":0.4},{"b":42.5,"et":2,"i":7,"f":0.32},{"b":43.0,"et":2,"i":7,"f":0.32},{"b":43.5,"et":2,"i":7,"f":0.32},{"b":44.0,"et":0,"i":1,"f":0.65},{"b":44.0,"et":1,"i":3,"f":0.55},{"b":44.0,"et":4,"i":1,"f":0.4},{"b":44.5,"et":2,"i":3,"f":0.32},{"b":45.0,"et":2,"i":3,"f":0.32},{"b":45.5,"et":2,"i":3,"f":0.32},{"b":46.0,"et":1,"i":3,"f":0.4},{"b":46.0,"et":4,"i":1,"f":0.4},{"b":46.5,"et":2,"i":3,"f":0.32},{"b":47.0,"et":2,"i":3,"f":0.32},{"b":47.5,"et":2,"i":3,"f":0.32},{"b":48.0,"et":0,"i":5,"f":0.95},{"b":48.0,"et":1,"i":6,"f":1.0},{"b":48.0,"et":2,"i":6,"f":0.85},{"b":48.0,"et":3,"i":6,"f":0.85},{"b":48.0,"et":4,"i":6,"f":0.95},{"b":48.24997732,"et":2,"i":7,"f":0.35},{"b":48.5,"et":2,"i":6,"f":0.55},{"b":48.5,"et":4,"i":7,"f":0.45},{"b":48.75002268,"et":2,"i":7,"f":0.35},{"b":49.0,"et":2,"i":6,"f":0.55},{"b":49.0,"et":4,"i":7,"f":0.45},{"b":49.24997732,"et":2,"i":7,"f":0.35},{"b":49.5,"et":2,"i":6,"f":0.55},{"b":49.5,"et":4,"i":7,"f":0.45},{"b":49.75002268,"et":2,"i":7,"f":0.35},{"b":50.0,"et":1,"i":7,"f":0.75},{"b":50.0,"et":2,"i":5,"f":0.55},{"b":50.0,"et":3,"i":5,"f":0.55},{"b":50.0,"et":4,"i":5,"f":0.6},{"b":50.24997732,"et":2,"i":7,"f":0.35},{"b":50.5,"et":2,"i":6,"f":0.55},{"b":50.5,"et":4,"i":7,"f":0.45},{"b":50.75002268,"et":2,"i":7,"f":0.35},{"b":51.0,"et":2,"i":6,"f":0.55},{"b":51.0,"et":4,"i":7,"f":0.45},{"b":51.24997732,"et":2,"i":7,"f":0.35},{"b":51.5,"et":2,"i":6,"f":0.55},{"b":51.5,"et":4,"i":7,"f":0.45},{"b":51.75002268,"et":2,"i":7,"f":0.35},{"b":52.0,"et":0,"i":1,"f":0.95},{"b":52.0,"et":1,"i":2,"f":1.0},{"b":52.0,"et":2,"i":2,"f":0.85},{"b":52.0,"et":3,"i":2,"f":0.85},{"b":52.0,"et":4,"i":2,"f":0.95},{"b":52.24997732,"et":2,"i":3,"f":0.35},{"b":52.5,"et":2,"i":2,"f":0.55},{"b":52.5,"et":4,"i":3,"f":0.45},{"b":52.75002268,"et":2,"i":3,"f":0.35},{"b":53.0,"et":2,"i":2,"f":0.55},{"b":53.0,"et":4,"i":3,"f":0.45},{"b":53.24997732,"et":2,"i":3,"f":0.35},{"b":53.5,"et":2,"i":2,"f":0.55},{"b":53.5,"et":4,"i":3,"f":0.45},{"b":53.75002268,"et":2,"i":3,"f":0.35},{"b":54.0,"et":1,"i":3,"f":0.75},{"b":54.0,"et":2,"i":1,"f":0.55},{"b":54.0,"et":3,"i":1,"f":0.55},{"b":54.0,"et":4,"i":1,"f":0.6},{"b":54.24997732,"et":2,"i":3,"f":0.35},{"b":54.5,"et":2,"i":2,"f":0.55},{"b":54.5,"et":4,"i":3,"f":0.45},{"b":54.75002268,"et":2,"i":3,"f":0.35},{"b":55.0,"et":2,"i":2,"f":0.55},{"b":55.0,"et":4,"i":3,"f":0.45},{"b":55.24997732,"et":2,"i":3,"f":0.35},{"b":55.5,"et":2,"i":2,"f":0.55},{"b":55.5,"et":4,"i":3,"f":0.45},{"b":55.75002268,"et":2,"i":3,"f":0.35},{"b":56.0,"et":0,"i":5,"f":0.95},{"b":56.0,"et":1,"i":6,"f":1.0},{"b":56.0,"et":2,"i":6,"f":0.85},{"b":56.0,"et":3,"i":6,"f":0.85},{"b":56.0,"et":4,"i":6,"f":0.95},{"b":56.24997732,"et":2,"i":7,"f":0.35},{"b":56.5,"et":2,"i":6,"f":0.55},{"b":56.5,"et":4,"i":7,"f":0.45},{"b":56.75002268,"et":2,"i":7,"f":0.35},{"b":57.0,"et":2,"i":6,"f":0.55},{"b":57.0,"et":4,"i":7,"f":0.45},{"b":57.24997732,"et":2,"i":7,"f":0.35},{"b":57.5,"et":2,"i":6,"f":0.55},{"b":57.5,"et":4,"i":7,"f":0.45},{"b":57.75002268,"et":2,"i":7,"f":0.35},{"b":58.0,"et":1,"i":7,"f":0.75},{"b":58.0,"et":2,"i":5,"f":0.55},{"b":58.0,"et":3,"i":5,"f":0.55},{"b":58.0,"et":4,"i":5,"f":0.6},{"b":58.24997732,"et":2,"i":7,"f":0.35},{"b":58.5,"et":2,"i":6,"f":0.55},{"b":58.5,"et":4,"i":7,"f":0.45},{"b":58.75002268,"et":2,"i":7,"f":0.35},{"b":59.0,"et":2,"i":6,"f":0.55},{"b":59.0,"et":4,"i":7,"f":0.45},{"b":59.24997732,"et":2,"i":7,"f":0.35},{"b":59.5,"et":2,"i":6,"f":0.55},{"b":59.5,"et":4,"i":7,"f":0.45},{"b":59.75002268,"et":2,"i":7,"f":0.35},{"b":60.0,"et":0,"i":1,"f":0.95},{"b":60.0,"et":1,"i":2,"f":1.0},{"b":60.0,"et":2,"i":2,"f":0.85},{"b":60.0,"et":3,"i":2,"f":0.85},{"b":60.0,"et":4,"i":2,"f":0.95}],"colorBoostBeatmapEvents":[{"b":2.0,"o":true},{"b":8.0,"o":false},{"b":24.0,"o":true},{"b":40.0,"o":false},{"b":48.0,"o":true}],"lightColorEventBoxGroups":[{"b":0.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":10.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":14.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":18.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":22.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":24.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":40.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":40.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":40.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":42.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":42.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":42.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":42.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":42.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":46.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":0.6,"i":0,"sb":0,"st":0}]}]},{"b":48.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.24997732,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.5,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.75002268,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":0,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":0,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":1,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":3,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":4.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":6.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":8.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":9.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":10.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":11.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":12.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":13.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":14.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":15.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":16.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":17.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":18.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":19.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":20.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":21.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":22.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":23.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":24.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":25.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":26.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":27.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":28.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":29.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":30.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":31.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":32.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":33.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":34.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":35.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":36.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":37.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":38.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":39.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":40.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":41.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":42.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":43.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":44.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":45.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":46.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":47.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":48.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":49.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":50.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":51.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":52.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":53.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":54.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":55.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":56.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":57.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":58.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]},{"b":59.0,"g":2,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"e":[{"b":0.0,"c":1,"s":1.0,"i":1,"sb":0,"st":0}]}]}],"lightRotationEventBoxGroups":[{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":12.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":0,"s":1.0,"t":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"l":[{"b":0.0,"p":0,"e":2,"l":30.0,"n":0,"d":1,"s":1.0,"t":0}]}]}],"lightTranslationEventBoxGroups":[{"b":0.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":4.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":8.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":12.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":16.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":20.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":24.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":26.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":28.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":30.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":32.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":34.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":36.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":38.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":40.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":44.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.04,"d":0}]}]},{"b":48.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":50.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":52.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":54.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":56.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":58.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]},{"b":60.0,"g":4,"e":[{"f":{"e":1,"l":0,"wf":1,"w":0,"xf":1,"x":0,"s":0,"t":1,"i":0,"b":1},"t":[{"b":0.0,"p":0,"e":2,"t":0.15,"d":0}]}]}],"vfxEventBoxGroups":[],"basicEventTypesWithKeywords":{},"useNormalEventsAsCompatibleEvents":true,"customData":{"_beatforgeTiming":{"schemaVersion":1,"clock":"constant-bpm-sample-projection","sampleRate":44100,"bpm":120.0}}}},"qa":{"status":"playtest_candidate","errors":[],"warnings":[],"metrics":{"infoVersion":2,"mapCount":5,"maps":{"EasyStandard.dat":{"version":3,"notes":20,"bombs":0,"walls":4,"arcs":0,"chains":0,"missingArcTails":0,"nextSameColorAfterHold":0,"postHoldMissingOppositeSetup":0,"rapidSameHandAfterHold":0,"likelyParityBreaks":0,"sameHandFlowConflicts":0,"crossHandPathCollisions":0,"unreachableTransitions":0,"bombNoteCollisions":0,"maxBeat":59.0,"difficulty":"Easy","characteristic":"Standard"},"NormalStandard.dat":{"version":3,"notes":20,"bombs":3,"walls":4,"arcs":0,"chains":0,"missingArcTails":0,"nextSameColorAfterHold":0,"postHoldMissingOppositeSetup":0,"rapidSameHandAfterHold":0,"likelyParityBreaks":0,"sameHandFlowConflicts":0,"crossHandPathCollisions":0,"unreachableTransitions":0,"bombNoteCollisions":0,"maxBeat":59.0,"difficulty":"Normal","characteristic":"Standard"},"HardStandard.dat":{"version":3,"notes":48,"bombs":2,"walls":1,"arcs":20,"chains":0,"missingArcTails":0,"nextSameColorAfterHold":0,"postHoldMissingOppositeSetup":0,"rapidSameHandAfterHold":0,"likelyParityBreaks":0,"sameHandFlowConflicts":0,"crossHandPathCollisions":0,"unreachableTransitions":0,"bombNoteCollisions":0,"maxBeat":59.5,"difficulty":"Hard","characteristic":"Standard"},"ExpertStandard.dat":{"version":3,"notes":112,"bombs":0,"walls":2,"arcs":54,"chains":0,"missingArcTails":0,"nextSameColorAfterHold":0,"postHoldMissingOppositeSetup":0,"rapidSameHandAfterHold":0,"likelyParityBreaks":0,"sameHandFlowConflicts":0,"crossHandPathCollisions":0,"unreachableTransitions":0,"bombNoteCollisions":0,"maxBeat":59.5,"difficulty":"Expert","characteristic":"Standard"},"ExpertPlusStandard.dat":{"version":3,"notes":105,"bombs":0,"walls":0,"arcs":47,"chains":0,"missingArcTails":0,"nextSameColorAfterHold":0,"postHoldMissingOppositeSetup":0,"rapidSameHandAfterHold":0,"likelyParityBreaks":0,"sameHandFlowConflicts":0,"crossHandPathCollisions":0,"unreachableTransitions":0,"bombNoteCollisions":0,"maxBeat":59.0,"difficulty":"ExpertPlus","characteristic":"Standard"}},"totals":{"notes":305,"bombs":5,"walls":11,"arcs":121,"chains":0,"missingArcTails":0,"nextSameColorAfterHold":0,"postHoldMissingOppositeSetup":0,"rapidSameHandAfterHold":0,"sameHandFlowConflicts":0,"crossHandPathCollisions":0},"clickTrackEvidence":{"source":"authored-score-checkpoints","checkpoints":[{"label":"start","beat":4,"sample":88200,"timeSeconds":2.0,"source":"synthetic-authored-score"},{"label":"middle","beat":30,"sample":661500,"timeSeconds":15.0,"source":"synthetic-authored-score"},{"label":"end","beat":56,"sample":1234800,"timeSeconds":28.0,"source":"synthetic-authored-score"}]},"releaseGate":{"structuralInspection":true,"vrPlaytest":false,"freshSightRead":false,"aiReleaseRoute":false}}},"summary":{"title":"Neon Rain","artist":"BeatForge synthetic score","bpm":120.0,"difficulties":{"Easy":{"notes":20,"arcs":0,"chains":0,"bombs":0,"walls":4},"Normal":{"notes":20,"arcs":0,"chains":0,"bombs":3,"walls":4},"Hard":{"notes":48,"arcs":20,"chains":0,"bombs":2,"walls":1},"Expert":{"notes":112,"arcs":54,"chains":0,"bombs":0,"walls":2},"ExpertPlus":{"notes":105,"arcs":47,"chains":0,"bombs":0,"walls":0}},"colorScheme":{"colorScheme":{"colorSchemeId":"BeatForgeSyntheticDemo","saberAColor":{"r":0.98,"g":0.62,"b":0.08,"a":1.0},"saberBColor":{"r":0.08,"g":0.88,"b":0.98,"a":1.0},"environmentColor0":{"r":0.98,"g":0.62,"b":0.08,"a":1.0},"environmentColor1":{"r":0.08,"g":0.88,"b":0.98,"a":1.0},"obstaclesColor":{"r":1.0,"g":0.38,"b":0.05,"a":1.0},"environmentColor0Boost":{"r":1.0,"g":0.8,"b":0.2,"a":1.0},"environmentColor1Boost":{"r":0.2,"g":0.95,"b":1.0,"a":1.0},"environmentColorW":{"r":1.0,"g":1.0,"b":1.0,"a":1.0},"environmentColorWBoost":{"r":1.0,"g":1.0,"b":1.0,"a":1.0}},"useOverride":true}},"timingEvidence":{"method":"Every oscillator and drum is placed at round(beat * 60 / bpm * sampleRate)","authoredSampleClock":true,"trackerAnalysisPerformed":false,"humanListeningVerified":false},"audioUrl":"assets/demo/song.ogg","downloadUrl":"assets/demo/map.zip","canRevise":false} diff --git a/web/assets/demo/song.ogg b/web/assets/demo/song.ogg new file mode 100644 index 0000000..d15ffc2 Binary files /dev/null and b/web/assets/demo/song.ogg differ diff --git a/web/connector.js b/web/connector.js new file mode 100644 index 0000000..ecd86ef --- /dev/null +++ b/web/connector.js @@ -0,0 +1,231 @@ +/* Private gateway session. Tokens live only in this closure, never in browser storage. */ +(() => { + 'use strict'; + let session = null, generation = 0; + const revisions = new Map(); + let uploads = new WeakMap(), submissions = new Map(), uploading = false, activeWatch = 0; + const $ = id => document.getElementById(id); + function origin(value) { + const url = new URL(value); + const local = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname); + if ((url.protocol !== 'https:' && !(local && url.protocol === 'http:')) || + url.username || url.password || url.search || url.hash || url.pathname !== '/') { + throw new Error('Use an HTTPS gateway origin, without a path or credentials. HTTP is allowed only on localhost.'); + } + return url.origin; + } + async function send(current, route, options = {}) { + if (!route.startsWith('/api/') || route.includes('\\')) throw new Error('Invalid gateway API path'); + const target = new URL(route, current.origin); + if (target.origin !== current.origin || !target.pathname.startsWith('/api/')) throw new Error('Invalid gateway API path'); + const headers = new Headers(options.headers); + headers.set('Authorization', `Bearer ${current.token}`); + const response = await fetch(target.href, {...options, headers, credentials:'omit', redirect:'error', cache:'no-store'}); + let result; + try { result = await response.json(); } catch { throw new Error('The gateway returned an unreadable response.'); } + if (!response.ok) { + const error = new Error(response.status === 401 ? 'Session rejected or expired. Reconnect with a valid token.' : + typeof result.detail === 'string' ? result.detail : Array.isArray(result.detail) ? result.detail.map(issue=>issue.msg).join(' ') : `Gateway request failed (${response.status}).`); + error.status = response.status; + throw error; + } + return result; + } + async function request(route, options) { + const current = session; + if (!current) throw new Error('Connect to your gateway first.'); + const result = await send(current, route, options); + if (session !== current) throw new Error('The connection changed; this result was discarded.'); + return result; + } + function downloadUrl(value) { + if (!session) throw new Error('Connect to your gateway first.'); + const url = new URL(value, session.origin); + if (url.origin !== session.origin || !url.pathname.startsWith('/downloads/') || url.username || url.password) + throw new Error('The gateway returned an invalid download URL.'); + return url.href; + } + window.BeatForgeConnection = { + get connected(){return Boolean(session);}, + get origin(){return session?.origin || null;}, + request, + async generate(form) { + if (uploading) throw new Error('An upload is already in progress.'); + if (form.get('engine') !== 'premium') throw new Error('Remote generation currently uses the Premium engine.'); + const audio = form.get('audio'); + if (!(audio instanceof File) || !audio.size || audio.size > 64*1024*1024) throw new Error('Choose an audio file up to 64 MiB.'); + uploading = true; + try { + const uploadSession = session; + let upload = uploads.get(audio); + let state; + if (upload) { + try { state = await request(`/api/uploads/${upload.id}`); } + catch (error) { + if (error.status !== 404 || session !== uploadSession) throw error; + // The gateway reclaimed an expired reservation. Other errors must + // remain visible instead of creating another upload or changing accounts. + upload = null; + } + if (state?.state === 'pending' && state.expires * 1000 <= Date.now()) upload = null; + if (!upload) uploads.delete(audio); + } + if (!upload) { + upload = await request('/api/uploads', {method:'POST', headers:{'Content-Type':'application/json'}, + body:JSON.stringify({filename:audio.name, size:audio.size})}); + uploads.set(audio, upload); + state = upload; + } + if (state.state === 'writing') throw new Error('This audio upload is still in progress. Retry when it finishes or times out.'); + if (state.state !== 'ready') { + $('gatewayStatus').textContent = 'Uploading audio directly to your gateway…'; + await request(upload.uploadPath, {method:'PUT', headers:{'Content-Type':'application/octet-stream'}, body:audio}); + } + const payload = {uploadId:upload.id, title:String(form.get('title')||audio.name), + artist:String(form.get('artist')||'Unknown Artist'), mapper:String(form.get('mapper')||'BeatForge'), + seed:Number(form.get('seed')||42), difficulties:String(form.get('difficulties')).split(','), + mappingPlan:JSON.parse(form.get('mappingPlan')||'{}'), allowUnconfirmed:$('gatewayUnconfirmed').checked}; + const key = JSON.stringify(payload); + if (!submissions.has(key)) submissions.set(key, crypto.randomUUID()); + const job = await request('/api/jobs', {method:'POST', headers:{'Content-Type':'application/json'}, + body:JSON.stringify({...payload, idempotencyKey:submissions.get(key)})}); + window.BeatForgeApp.showRemoteJob(job); + window.dispatchEvent(new Event('beatforge:history')); + await refresh().catch(()=>{}); // The durable job already exists even if history refresh fails. + const current = session; + const watch = ++activeWatch; + const poll = async () => { + if (!session || session !== current || watch !== activeWatch) return; + try { + const latest = await request(`/api/jobs/${job.id}`); + window.BeatForgeApp.showRemoteJob(latest); + $('gatewayStatus').textContent = `Generation ${latest.state}. ${latest.result?.message || 'Waiting for your worker. You can cancel from My runs.'}`; + if (['queued','running'].includes(latest.state)) setTimeout(poll, 2000); + else { + window.dispatchEvent(new Event('beatforge:history')); await refresh(); + if (latest.result?.status === 'playtest_candidate') await window.BeatForgePreview.loadRemoteJob(latest.id, latest.request.difficulties[0]); + } + } catch(error) { if (session === current) $('gatewayStatus').textContent = `${error.message} Refresh runs to check this job.`; } + }; + void poll(); + return job; + } finally { uploading = false; } + }, + async revise(id, payload) { + const key = id + JSON.stringify(payload); + if (!revisions.has(key)) revisions.set(key, crypto.randomUUID()); + const result = await request(`/api/jobs/${encodeURIComponent(id)}/revise`, { + method:'POST', headers:{'Content-Type':'application/json'}, + body:JSON.stringify({...payload, idempotencyKey:revisions.get(key)}) + }); + const current = session; + const poll = async () => { + if (!session || session !== current) return; + try { + const job = await request(`/api/jobs/${result.id}`); + $('gatewayStatus').textContent = `Section revision ${job.state}. ${job.result?.message || 'The original map is preserved. Your original worker must be online.'}`; + window.dispatchEvent(new Event('beatforge:history')); + if (['queued','running'].includes(job.state)) setTimeout(poll, 2000); + else { + await refresh(); + if (job.result?.status === 'playtest_candidate' && window.BeatForgePreview.getState()?.remoteJobId === id) + await window.BeatForgePreview.loadRemoteJob(job.id, payload.difficulty); + } + } catch(error) { if (session === current) $('gatewayStatus').textContent = `${error.message} Refresh runs to check the revision.`; } + }; + void poll(); + return result; + }, + async preview(id, difficulty) { + const query = difficulty ? '?difficulty=' + encodeURIComponent(difficulty) : ''; + const [payload, job] = await Promise.all([ + request(`/api/jobs/${encodeURIComponent(id)}/preview${query}`), + request(`/api/jobs/${encodeURIComponent(id)}`) + ]); + window.BeatForgeApp.showRemoteJob(job); + return {...payload, audioUrl:downloadUrl(payload.audioUrl)}; + } + }; + const panel = document.createElement('section'); + panel.className = 'panel gateway-panel'; + panel.setAttribute('aria-labelledby', 'gatewayHeading'); + panel.innerHTML = `
Connect your assistant workspace +

Open maps created through your BeatForge connector. Your gateway token stays in this tab until you disconnect or reload.

+
+ + + +
+
+
+ +

Private token connection · OAuth sign-in is not available in this Studio yet.

+
+
`; + $('previewPanel').before(panel); + async function refresh() { + const runs = await request('/api/jobs'); + $('gatewayRuns').replaceChildren(); + for (const run of runs.jobs) { + const row = document.createElement('div'); row.className = 'job-row'; + const info = document.createElement('div'); info.className = 'job-info'; + const title = document.createElement('strong'); title.textContent = run.request.title; + const detail = document.createElement('small'); detail.textContent = `${run.request.artist} · ${run.result?.status || run.state}${run.request.revision ? ' · section revision' : ''}`; + info.append(title, detail); row.append(info); + if (run.state === 'completed' && run.result?.status === 'playtest_candidate') { + const open = document.createElement('button'); open.className = 'secondary'; open.textContent = 'Open preview'; + open.addEventListener('click', async () => { + open.disabled = true; + try { + window.BeatForgeControls.applyPlan(run.request.mappingPlan); + const loaded = await window.BeatForgePreview.loadRemoteJob(run.id, run.request.difficulties[0]); + if (loaded) $('previewPanel').scrollIntoView({behavior:'auto'}); + } catch(error) { $('gatewayStatus').textContent = error.message; } + finally { open.disabled = false; } + }); + const save = document.createElement('button'); save.className = 'secondary'; save.textContent = 'Get map ZIP'; + save.addEventListener('click', async () => { + save.disabled = true; + try { + const result = await request(`/api/jobs/${run.id}/artifacts`); + const artifact = result.artifacts.find(item => item.name === 'map.zip'); + if (!artifact) throw new Error('This run has no published map ZIP.'); + const link = document.createElement('a'); link.href = downloadUrl(artifact.downloadUrl); + link.textContent = 'Download ZIP · link expires in 5 minutes'; link.referrerPolicy = 'no-referrer'; + link.className = 'secondary'; save.replaceWith(link); + } catch(error) { $('gatewayStatus').textContent = error.message; save.disabled = false; } + }); + row.append(open, save); + } + $('gatewayRuns').append(row); + } + if (!runs.jobs.length) $('gatewayRuns').textContent = 'No assistant runs yet. Create a map through your connected assistant.'; + } + $('gatewayConnect').addEventListener('submit', async event => { + event.preventDefault(); const ticket = ++generation; + $('gatewaySubmit').disabled = true; $('gatewayStatus').textContent = 'Checking gateway…'; + try { + const candidate = {origin:origin($('gatewayOrigin').value.trim()), token:$('gatewayToken').value.trim()}; + if (candidate.token.length < 32 || /[^\x21-\x7e]/.test(candidate.token)) throw new Error('Enter a valid gateway access token.'); + const capability = await send(candidate, '/api/capabilities'); + if (ticket !== generation) return; + session = candidate; $('gatewayToken').value = ''; + window.dispatchEvent(new Event('beatforge:connection')); + $('gatewayConnect').hidden = true; $('gatewayRefresh').hidden = false; $('gatewayDisconnect').hidden = false; + $('gatewayTimingChoice').hidden = false; + $('gatewayStatus').textContent = `Connected to ${candidate.origin}. ${capability.generationMessage}`; + await refresh(); + } catch(error) { if (ticket === generation) $('gatewayStatus').textContent = error.message; } + finally { $('gatewaySubmit').disabled = false; } + }); + $('gatewayRefresh').addEventListener('click', () => refresh().catch(error => {$('gatewayStatus').textContent = error.message;})); + $('gatewayDisconnect').addEventListener('click', () => { + ++generation; ++activeWatch; session = null; revisions.clear(); uploads = new WeakMap(); submissions.clear(); + window.BeatForgePreview.clearRemote(); + window.dispatchEvent(new Event('beatforge:connection')); + $('gatewayToken').value = ''; $('gatewayRuns').replaceChildren(); + $('gatewayConnect').hidden = false; $('gatewayRefresh').hidden = true; $('gatewayDisconnect').hidden = true; + $('gatewayTimingChoice').hidden = true; $('gatewayUnconfirmed').checked = false; + $('gatewayStatus').textContent = 'Disconnected. Your token has been discarded.'; + }); +})(); diff --git a/web/index.html b/web/index.html index 763ba3c..0ca0ceb 100644 --- a/web/index.html +++ b/web/index.html @@ -51,10 +51,8 @@ .hero-copy, .panel { border: 1px solid var(--line); background: linear-gradient(145deg, rgba(24,27,34,.96), rgba(14,16,21,.96)); border-radius: var(--radius); } .hero-copy { padding: clamp(18px, 2.4vw, 28px); min-height: 0; display: grid; grid-template-columns: minmax(200px, 1fr) minmax(300px, 460px); gap: 22px; align-items: center; position: relative; overflow: hidden; } h1 { display: flex; justify-content: center; align-items: center; margin: 0; width: 100%; min-height: clamp(140px, 22vw, 210px); font-size: clamp(38px, 5.2vw, 72px); } - .hero-logo { display: grid; place-items: center; width: min(460px, 100%); height: clamp(140px, 22vw, 210px); overflow: hidden; margin: 0 auto; - -webkit-mask-image: radial-gradient(ellipse 78% 72% at 50% 48%, #000 46%, transparent 78%); - mask-image: radial-gradient(ellipse 78% 72% at 50% 48%, #000 46%, transparent 78%); } - .hero-logo img { display: block; width: 100%; height: 100%; object-fit: contain; object-position: center; mix-blend-mode: screen; filter: saturate(1.08) contrast(1.06); } + .hero-logo { display: grid; place-items: center; width: min(460px, 100%); height: clamp(140px, 22vw, 210px); margin: 0 auto; } + .hero-logo img { display: block; width: 100%; height: 100%; object-fit: contain; object-position: center; } .panel { padding: 22px; } .hero-form { min-width: 0; padding: 0; border: 0; background: transparent; } .hero-form .dropzone { min-height: 88px; padding: 18px 16px; } @@ -210,17 +208,21 @@ button:focus-visible, input:not([type="file"]):focus-visible, select:focus-visible, a:focus-visible { outline-width: 3px; } } +
+ Preview + My runs + Improve Checking Beat Saber library

- +

@@ -273,7 +275,7 @@

Start a mapping run

-

Generate always packs a download. Unverified timing does not block it. The AI timing buttons are an optional check, not a gate. Release candidate is not required to download. BeatForge will not fill either gap silently.

+

Generate analyzes timing and checks the chart. If timing needs confirmation, use anchors or explicitly continue unverified. Structural errors block installation. BeatForge will not fill either gap silently.

@@ -285,16 +287,44 @@

Start a mapping run

Give the agent musical intent in plain language. It can apply a typed plan, start the visible run, read safety gates, and record evidence you explicitly supply after playing. The agent never receives your song audio through these tools and never invents a clearance.

+
+ + + + + + + + +
+
Try “lighter verses, denser chorus, follow drums, no bombs”. Explicit controls override the brief. Other prose is saved as a note.
+
+
Generation engine

Premium uses musical controls and deterministic constraint solving.

+
- The demo uses a synthetic 30-second groove and does not create a Beat Saber pack. + Explore a real synthetic 30-second groove and download its sample map.
-
+
Connection details and activity
Exposed tools
Waiting for WebMCP…
Activity
Human actions and agent calls will appear here.
-
+
+
+
Listen, inspect, revise

Map preview

Load a sample or completed run
+

Hear the song. See the movement.

+ The chart preview requires a browser with canvas support. + +
0:00 / 0:00
+ +
+
Revise a section

Complete a local mapping run to revise its sections.

+
+ +

Preview geometry helps inspect a chart. Beat Saber scoring, spawn behavior and headset comfort still need a real playtest.

+
+

My runs

Your local runs will appear here.
@@ -304,7 +334,7 @@

Start a mapping run

No events yet.