Commit Graph
1175 Commits
Author SHA1 Message Date
aj ca1902e075 fix(bestfit): stop at internal arc tangency 2026-09-27 16:35:53 -04:00
aj bc5fd86996 test(fill): characterize unchanged-row validation reuse 2026-09-27 15:10:01 -04:00
aj 1b23ad79f2 perf(fill): reuse part triangulations within an overlap check
After 1b, triangulating both polygons on every pair was the largest
remaining overlap cost (27% of main-thread samples on the corpus job).
PartOverlapChecker now triangulates each part at most once per check,
lazily after the bounding-box gate, and passes the triangles to a new
internal Collision.HasOverlap overload that runs the unchanged
OverlapRegions body. Triangles are only read by clipping and hole
subtraction, so reuse gives identical verdicts.

Verification:
- 49,000 seeded decisions with reused triangles match LegacyCollision;
  triangles stay bit-identical to a fresh triangulation afterwards.
- Debug PolygonTriangulations: 246 -> 40 and 64 -> 36 per grid check;
  sharing triangles per Program instead fails 23 tests.
- Corpus job (169 parts, --engines Default --parallel 1): median
  13,398 -> 12,702 ms over 4+4 alternating runs vs 1b, identical
  outcomes; serialized layout byte-identical to the base.

Also records the Follow-up B' (Slices 1a, 1b, 2a) measurements in
docs/performance/fill-performance.md.
2026-09-27 13:49:46 -04:00
aj a27290a29c perf(fill): prepare overlap polygons once per check
Both HasOverlappingParts loops rebuilt each part's polygon from its
Program on every pair. PartOverlapChecker prepares each distinct Program
(reference identity) once and each part's world polygon once per call,
then uses the overlap-only Collision.HasOverlap. Loop order, bounding-box
prefilter, early exit and returned indices are unchanged; Part.Intersects
shares the material/polygon recipe and still returns crossing points.

Verification:
- Frozen LegacyPartOverlap differential (original Intersects and both
  loops): verdicts, indices and world polygons bit-identical across fill
  grids, patterns, touching/epsilon gaps, scribe/rapid/empty programs.
- Debug OverlapPolygonPreparations: 246 -> 1 and 64 -> 2 per check.
- Corpus job (169 parts, --engines Default --parallel 1, with 1a):
  median 18,885 -> 13,464 ms over 4+4 alternating runs, identical
  outcomes; serialized layout byte-identical to the base.
2026-09-27 13:34:04 -04:00
aj f44a1a3d2c style(console): consolidate usage text into one multiline write 2026-09-27 13:17:35 -04:00
aj 2b5485f6cf perf(core): skip unused crossing points in overlap-only checks
Collision.HasOverlap only needs the verdict, but it went through Check,
which also collected crossing points. Triangulation, clipping and hole
subtraction now live in one private OverlapRegions method shared by Check
and HasOverlap, so verdict arithmetic stays single-sourced; Check output is
unchanged.

Tests: a frozen copy of the previous Collision is the oracle. 50,000 seeded
HasOverlap verdicts and 2,400 bitwise Check results match it, plus
containment, contact, hole and input-immutability cases. A Debug-only
PerfCounters.CrossingPointScans counter proves HasOverlap no longer scans.
Malformed polygons with null outer vertices still throw when the bounding
boxes overlap (now ArgumentNullException from triangulation rather than
NullReferenceException from ToLines).

Measured (Release, same harness in both trees): about 44% less time per
overlap-only polygon check, allocations 10.0 -> 7.9 MB per 155-pair sweep.
The 169-part serialized corpus layout is byte-identical.
2026-09-27 12:54:20 -04:00
aj a98a49c00a test(fill): add opt-in overlap-check micro benchmark
Times Collision.HasOverlap over the box-overlapping neighbour pairs of two
FillLinear grids, and the FillHelpers.HasOverlappingParts grid checks
themselves. Opt-in via OPENNEST_RUN_FILL_PERF=1 (Category=FillPerformance).
Uses only APIs that predate the overlap-check work, so the same file can be
copied into a before tree for same-harness comparisons.
2026-09-27 12:54:20 -04:00
aj fdbc1fb99f style(console): combine adjacent output with readable multiline strings 2026-09-27 12:48:52 -04:00
aj 82feb78b0f feat(posts): add Cincinnati CI Fiber (TF5200) post-processor
New post-processor plugin OpenNest.Posts.CincinnatiCIFiber for the CI
Fiber laser family (nLight CLX / Beckhoff TF5200 / Precitec ProCutter,
e.g. the CI Fiber 4020 8kW). Named by machine family, not table size.

Emits the machine program contract of the Cincinnati-supplied sample NC
(12992-4SS_NEST.nc): V.E.* header, restart jump, per-part V.E.R4 blocks,
per-contour N labels with V.E.R3, skippable /L macro lines (L0/L2+G41
interior, L4+G42 exterior, L6 cut-on, ZHSOFF cut-end), G162 incremental
arc I/J, trimmed 3-decimal spaceless coordinates, CRLF, M50/M30/%.

Contour classification (interior vs exterior) derives from the material
side of the closed cut path, not hardcoded winding. SubProgramCall holes
are flattened to sheet coordinates (rotation-safe). Arc lead-ins are
rejected per TF5200 13.2.4.1 (first motion block after G41/G42 selection
must be linear). Table envelope (default 160.25 x 81.25 in) validated.

Tests: structure golden on a square-with-hole nest, rotated-hole flatten,
coordinate format, arc-lead-in rejection, table validation, suppressed/
scribe skipping, plus a SkippableFact regression against the real sample
NC (109 parts, 2071 contours, L2=1962, L4=109, perimeter vertices match
within 0.001). Fixtures configure through OpenNest.Tests/test-config.json
and the regression skips when absent.
2026-09-27 04:10:22 -04:00
aj c825f40213 refactor(posts): move post-processor projects under Posts/
Group the Cincinnati and GravographIS plugin projects in a Posts/
folder so new machine posts have one home. Project names, namespaces,
and the runtime Posts/ deploy target are unchanged; only relative
paths in the solution and project references move.
2026-09-26 22:35:23 -04:00
aj 0df2587cf2 perf(fill): reuse offset geometry for translated copies
FillLinear re-prepared offset perimeter geometry (ConvertProgram ->
ShapeProfile -> OffsetOutward) for every part it measured, although
tiled copies share one Program and differ only by Location. A CPU
profile of a 169-part Default job put 62% of wall time there.

Prepare each distinct Program (reference identity) once per public
Fill/FillRow call in local frame, then clone and translate for each
location. The cache is created per call and passed down privately
because FillHelpers.FillPattern calls Fill concurrently on one
instance. PartGeometry gains a local-frame Program overload that the
Part overload now delegates to.

Evaluation order, lazy preparation, fallbacks and tiling are
unchanged. Differential tests against a frozen copy of the previous
FillLinear check bitwise equality, including concurrent calls; Debug
work tests pin preparation counts. With the thread pool capped at one
worker, before/after whole-job layouts are byte-identical. The
Default corpus job median drops from 40,715 to 18,810 ms.
2026-09-26 20:24:34 -04:00
aj 094c4c196b fix(bestfit): never reject pair candidates by utilization
Thin-framed, hollow, or concave parts (e.g. SULLYS-035's frame) have
inherently low part-to-bbox utilization yet nest tightly, so the 30%
MinUtilization floor wrongly dropped every candidate for them. Pair
quality is judged by the rotated pair bounding-box area the results
are already sorted on; utilization now only ever serves as the
high-aspect exception (UtilizationOverride), never as a rejection.

Adds a hollow-frame helper plus regression tests that kept pairs
exist with low utilization and results stay sorted by pair area.
2026-09-26 14:14:44 -04:00
aj 77b729e58c docs(readme): drop opt-in fill performance measurements section
Keeps the README approachable for new users; microbenchmark details
remain in docs/performance/ for maintainers.
2026-09-26 13:00:32 -04:00
aj 1d1c60daa7 docs(fill): record combined initial-batch acceptance 2026-09-26 11:25:17 -04:00
aj 1e8e532063 perf(fill): skip feature extraction without an angle model 2026-09-26 10:02:15 -04:00
aj d70505b7c0 docs(fill): sync shared guidance for scalar-only extraction 2026-09-26 07:36:25 -04:00
aj 8188533d72 perf(ml): support scalar-only angle features 2026-09-26 00:02:28 -04:00
aj 6863c8bdb1 docs(fill): clarify Task 3 evidence retention 2026-09-25 21:46:31 -04:00
aj 4553f8afad perf(fill): avoid redundant bounds recomputation 2026-09-25 21:45:03 -04:00
aj 4ec92c95ec docs: share coding-agent guidance through AGENTS.md 2026-09-25 20:03:42 -04:00
aj b2c864a328 docs(fill): synchronize performance workflow guidance 2026-09-25 19:59:02 -04:00
aj 6efa6b1117 perf(fill): remove discarded extents pitch geometry 2026-09-25 19:52:19 -04:00
aj cec7396da6 perf(fill): avoid unused scores with custom comparers 2026-09-25 17:18:15 -04:00
aj b318950a54 perf(fill): short-circuit default comparisons by count 2026-09-25 16:43:09 -04:00
ajandClaude Opus 5.5 1b862dc1a8 fix(engine): allow 0.0005 spacing slack in layout validation
Layouts placed exactly at the part spacing can land ~1e-4 short once
rotated, rounded (e.g. PEP's 4-decimal exports) and snapped to the
Clipper grid, so both validators rejected layouts that were correct in
practice. NestTolerances.SpacingSlack (0.0005, far below anything a
cutting machine resolves) is now subtracted from the spacing by
NestLayoutCheck's inflation and NestJobPlacementValidator's edge-distance
check. The frozen LegacyNestValidator takes the same rule so the
equivalence tests keep comparing like with like.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:19:04 -04:00
aj d1f3907505 Merge branch 'fix/mcp-config-path' 2026-09-25 14:19:01 -04:00
ajandClaude Opus 5.5 4fb924b03a chore: restore LF line endings in CLAUDE.md
39db4dc committed CLAUDE.md with CRLF endings while the repo stores it
as LF (text=auto), turning a one-line doc addition into a whole-file
diff. Renormalized; the content is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:12:40 -04:00
ajandClaude Opus 5.5 39db4dc10f feat(ui): route Auto Nest through whole-job engines
StockLadder and Engines/ plug-ins only implement INestingEngine.Solve, so
selecting them in Auto Nest had no path to run. MainForm now solves the
whole job through JobEngineNest when the selected engine is not a
built-in fill strategy, feeding NestJobProgress into NestProgressForm and
binding the result poses back onto the nest's own drawings. Whole-job
engines throw on cancel rather than returning a partial layout, so the
progress form hides Accept for these runs. Built-in strategies keep the
existing per-plate fill path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:03:10 -04:00
ajandClaude Opus 5.5 23dd99fa2f feat(benchmark): add --progress logging for engine solves
Long whole-job solves ran silently, so there was no way to tell a slow
engine from a hung one until the timeout fired. --progress hands each
solve a JobProgressLog that prints [job/engine] lines for start, finish
(or failure/timeout), every plate commit, and candidate evaluations
throttled to one line per 2 s so parallel runs stay readable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:03:10 -04:00
695ccc0a3b perf(engine): use cached triangulations in the layout check
The benchmark validator and every engine test re-triangulated both parts
for each nearby pair. NestLayoutCheck now uses TriangulatedRegion, with
Collision.HasOverlap as the fallback when it cannot decide. Verdicts are
unchanged (the frozen-validator equivalence tests still pass); validating
100 discs went from 1,254 ms to 94 ms.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:08:36 -04:00
98d0124172 perf(core): add cached-triangulation collision and an edge-grid prefilter
Collision.HasOverlap re-triangulates both polygons on every call; Qwen
measured that as its dominant cost (over 400 s -> ~110 s on a 219-part
job once cached). TriangulatedRegion (from Qwen's TriSet) triangulates a
part once and takes translation as a parameter; it returns null when it
cannot decide so callers fall back to Collision, which stays the
reference. EdgeGridPolygon (from Qwen's FastPoly) certifies clearly
disjoint shells and never reports Clear for an overlap. A seeded harness
of 100,000 decisions (concave shapes, arcs, holes, touching contacts)
finds 0 mismatches against Collision.HasOverlap.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:08:36 -04:00
4161e6d1c7 feat(core): add a concave no-fit polygon to NoFitPolygon
Core only had a convex NFP, so Opus55 and Gpt6Astra each built concave
NFPs from Clipper's Minkowski sum, and only Opus55 added the terms that
cover one part lying inside or swallowing the other - Gpt6Astra instead
filled every positive path and lost real interlocks. NoFitPolygon.Compute
ports Opus55's construction (boundary sweep united with A + p0 and
-B + a0; convex pairs use the linear edge merge). It works on filled
perimeters only; hole-aware clearance stays with collision testing.
Tests port Opus55's NFP tests and add a notch fit and a seeded property
check against Collision.HasOverlap.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:08:36 -04:00
896ed2026a fix(engine): leave scribe marks out of layout bounds and salvage
The job runner already checked sheet bounds on material contours only,
but the benchmark validator and salvage scoring used Part.BoundingBox,
which includes scribe/etch moves. A PEP bend tick that ends a hair past
the part's edge passed the runner yet failed the benchmark when placed
flush to the sheet edge, and it could shrink the credited offcut. Marks
only mark the surface, so bounds and salvage now use material only.

Benchmark before/after (all five built-in engines, local fixtures,
salvage 0.5): no job changed validity or cost. Regression tests pin the
new rule: a protruding tick flush to the sheet edge is valid in all four
quadrants, and a tick past the parts envelope no longer shrinks salvage
(targeted fixture cost 130 -> 120).

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:56:49 -04:00
7f63c725e6 refactor(engine): expose the layout validation contract to engines
Engines had to reverse-engineer the benchmark validator: Opus55 assumed a
0.01 arc tolerance (the validator uses 0.001), Gpt6Astra added hand-tuned
paddings and copied the validator's check order, Qwen picked its chord
tolerance to stay under a constant it could not reference.

NestTolerances publishes the validator's arc tolerance, the Clipper grid
and SafeClearanceMargin (with its derivation). NestLayoutCheck moves the
benchmark NestValidator's checks into OpenNest.Engine as a public API
(Clears for a part pair, Violations for a whole result); NestValidator is
now a thin wrapper. Verdicts are unchanged: tests compare ordered
violation lists against a frozen copy of the old validator, and a
tangent-disc stress test covers 432 pairs at the safe margin.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:54:54 -04:00
ec5f57171f feat(engine): add RotationPolicy.EnumerateAngles and RotationCandidates
All three plugin engines turned a RotationPolicy into trial angles by hand
(fixed angle, stepped sweep, or right angles plus the minimum-bounding-
rectangle angle for Automatic), each with its own normalization, dedup and
sweep caps. EnumerateAngles gives one deterministic, Allows-checked list;
RotationCandidates.ForShape adds the MBR-aligning angles via the existing
Polygon.FindBestRotation, and DistinctOutlines drops angles where the part
looks identical. A cap of one returns the sweep start rather than throwing,
since engines request a single sample for small orientation budgets.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:33:52 -04:00
dd1a958f5c feat(engine): expose NestJobCost, the benchmark's scoring
Engines optimized guesses at the benchmark cost: Opus55 re-implemented
salvage credit, Qwen used plate area per part area, Gpt6Astra ignored
salvage. NestJobCost moves StockLadder's EstimateNetArea into a public
home (net sheet area, unplaced-part penalty, whole-result Evaluate) and
the benchmark and StockLadder now call it. Scores are unchanged: tests pin
it against a frozen copy of the old computation and real benchmark runs.
Bounds still include marks, as before, so scores do not move.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:12:07 -04:00
ae2b0beb45 feat(engine): expose JobPartGeometry for reading snapshot material
Every plugin engine rebuilt part material from a snapshot by hand and
filtered only rapids, so all three kept counting scribe/etch marks as
material after 1b5e1b1 fixed it in the host. JobPartGeometry is the
validator's own reader made public: SpecialLayers.IsMaterial, validated
closed contours, material area, and TryRead returning null for unreadable
parts. The job validators now use it, so engines and validation agree.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:12:07 -04:00
ajandClaude Opus 5.5 6ab45e6de7 test(engine): seed spacing brute-force sampling deterministically
The seed came from string.GetHashCode, which .NET randomizes per process,
so each run drew different samples and the "ring" case occasionally drew
fewer than six rejections and failed its coverage assertion (1 in 6 runs),
even though every validator decision matched the brute-force reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:12:07 -04:00
029299ccf8 feat(engine): add NestJobResultBuilder for engine result assembly
Engines assembled NestJobResult by hand - instance indices, fulfillment,
stock usage, status and PlateCommitted progress - and Qwen38FlashNext got
PlateIndex wrong (stock index instead of sheet order). The builder assigns
plate and instance indices itself and rejects overproduction and exhausted
stock, so engines only decide placements.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:55:10 -04:00
4cdb39870b feat(engine): expose NestPlateStock.WorkArea, Area and Fits
Every plugin engine re-derived the quadrant/edge-spacing work area by hand
(Gpt6Astra, Opus55 and Qwen each had a copy, as did the placement
validator). One definition on the stock removes that duplication and the
chance of an engine disagreeing with the validator's bounds.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:55:10 -04:00
ajandCodex 95833236cf docs(engine): clarify priority so scarce stock favors lower numbers
Document constructor and property semantics and cover priority zero versus nine on one sheet.

Co-Authored-By: Codex <noreply@openai.com>
2026-09-25 07:53:16 -04:00
ajandClaude Opus 5.5 1b5e1b14a6 fix: leave etch/scribe marks out of nesting geometry
Every nesting-geometry consumer filtered only rapids, so scribe/etch
moves counted as part material. An etch tick that ends a hair outside
the outline (PEP bend ticks start on the notch edge) made the part
"open geometry leaving the material region": the job validator threw
and every built-in engine plus Gpt6Astra crashed on real PEP jobs
(PT75, drawing 4980 A01 PT77). Marks are only on the surface, so they
should never affect placement, collision, area, or validation.

- SpecialLayers.IsMaterial excludes Rapid and Scribe; used by drawing
  area, canonical angle, part collision, PartGeometry, plate perimeter,
  best-fit/pair evaluation, rotation analysis, GPU evaluators, and both
  validators. Timing, display, splitting and posts still see marks.
- ConvertGeometry also maps the saved SCRIBE layer name to Scribe, so
  programs rebuilt from stored entities keep their marks.
- NestReader repairs older files (e.g. PepNestExport output) whose
  programs saved etch as cut moves while source entities kept SCRIBE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 06:44:29 -04:00
ajandClaude Opus 5.5 e3d10e95ae chore: move plugin engines to the OpenNest-Engines repo
Engines now live in https://git.thecozycat.net/aj/OpenNest-Engines (history
carried over) so they can be published independently, and so a copy of
OpenNest handed to a model for an engine-building run contains no
competing engines. Engines still load at runtime from an Engines/ folder
next to the app/benchmark output; nothing in the solution referenced them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 10:02:29 -04:00
aj 22d21924b8 feat(astra): add independent nesting engine and fix hole validation
Replace the Terra scaffold with an independent configuration-space contact placer and bounded stock-plan search. Include plugin tests, synthetic and DXF benchmark drivers, results, and deployment documentation.

Correct shared collision clipping and hole subtraction so curved-hole inserts validate consistently. Cover translated layouts, spacing violations, operand order, winding, and independent boolean-area comparisons.

Validation: 1,293 tests passed with 12 fixture skips; all 34 synthetic/generated and four DXF cases are valid and complete.
2026-09-24 00:14:36 -04:00
ajandClaude Sonnet 5 87f7ddad1a docs(engines): allow fill/pattern components in Terra and Qwen scaffolds
Reusable Fill/, BestFit, RectanglePacking and CirclePacking components are
fair game; whole-engine delegation and run-all-pick-best stay banned.
Improvements to shared components go in the engine's own project and are
reported, not applied to OpenNest.Core/OpenNest.Engine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 19:15:19 -04:00
aj 23fcec4b6c docs: compress README to a skimmable overview
Drop deep engine/validator/benchmark internals (covered by CLAUDE.md and
code comments) and per-option reference tables in favor of a quick-start
level guide.
2026-09-23 17:37:07 -04:00
aj d6d819bfed chore: remove tools directory
One-off helper utilities don't belong in the repo. PepNestExport moved to
/home/aj/src/PepNestExport as a standalone companion project; NestDxfJob and
StreamGravographJob are removed (recoverable from history).
2026-09-23 15:15:45 -04:00
aj c45909e397 Merge branch 'perf/fill-pipeline' 2026-09-23 14:30:46 -04:00
ajandClaude Opus 5.5 1c363504f5 perf(engine): key fill caches by source drawing so they hit across trials
Every DefaultPlateFiller.Fill makes a fresh canonical copy of the drawing,
and BestFitCache/FillResultCache keyed by drawing reference, so fills never
shared results and the static caches grew without bound.

CanonicalFrame now records which drawing each canonical copy came from.
Both caches key weakly on that source drawing, so every canonical copy
shares one entry and released drawings can be collected. An entry is
dropped when the drawing's Program instance or canonical angle changes.
Best-fit candidates are computed once per (drawing, spacing) through
BestFitFinder.FindCandidates and filtered per plate size with the same
filter FindBestFits uses. FillResultCache keeps canonical and
non-canonical callers apart.

Adds Debug-only PerfCounters for best-fit runs, offset perimeter builds
and Part.Intersects calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:29:43 -04:00
ajandClaude Opus 5.5 dc9e83ef17 perf(jobs): flatten validator contours once, at a 0.001 chord tolerance
The candidate validator flattened every arc into 1000 segments and rebuilt
both parts' polygons and edge lists for every pair it compared, so spacing
checks on filleted parts cost millions of edge pairs each. Validation, not
the fill pipeline, was nearly all of a solve's wall time.

Placed contours are now flattened once with ToPolygonWithTolerance(0.001),
the tolerance the benchmark NestValidator and Part.Intersects already use,
and each part's shape is built once per candidate. Arcs stay inscribed, so
a layout placed exactly at the spacing still passes.

12-nest PEP corpus, Default + StockLadder, --parallel 1: 2820 s -> 227 s.
Every run that finished before gives the same validity, count, plates and
cost. Three StockLadder runs that used to hit the 5-minute timeout now
finish.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:29:43 -04:00