• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pcb2gcode / pcb2gcode / 21021097492

15 Jan 2026 05:42AM UTC coverage: 70.124% (+10.1%) from 60.064%
21021097492

push

github

web-flow
Merge pull request #752 from eyal0/integration_tests_in_ci

ci: Include integration tests as part of coverage.

4430 of 7541 branches covered (58.75%)

Branch coverage included in aggregate %.

3710 of 4067 relevant lines covered (91.22%)

15236187.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.05
/path_finding.cpp
1
#include <vector>
2
using std::vector;
3

4
#include <unordered_map>
5
using std::unordered_map;
6

7
#include <unordered_set>
8
using std::unordered_set;
9

10
#include <queue>
11
using std::priority_queue;
12

13
#include <utility>
14
using std::pair;
15
using std::make_pair;
16

17
#include <boost/graph/adjacency_list.hpp>
18
#include <boost/graph/astar_search.hpp>
19
#include <boost/optional.hpp>
20

21
#include "path_finding.hpp"
22
#include "options.hpp"
23
#include "geometry.hpp"
24
#include "bg_operators.hpp"
25
#include "bg_helpers.hpp"
26
#include "segment_tree.hpp"
27

28
namespace path_finding {
29

30
using boost::optional;
31
using boost::make_optional;
32

33
Neighbors::iterator Neighbors::iterator::operator++() {
36,621✔
34
  const auto& all_vertices_size = neighbors->vertices.size();
36,621✔
35
  do {
36
    // Move to a new valid point, even if it isn't a neighbor.
37
    point_index++;
73,242✔
38
  } while (point_index < all_vertices_size + 2 &&
73,242!
39
           !neighbors->is_neighbor(**this));
73,242✔
40
  return *this;
×
41
}
42

43
bool Neighbors::iterator::operator!=(const Neighbors::iterator& other) const {
×
44
  return (point_index != other.point_index);
×
45
}
46

47
bool Neighbors::iterator::operator==(const Neighbors::iterator& other) const {
×
48
  return !(*this != other);
×
49
}
50

51
const point_type_fp& Neighbors::iterator::operator*() const {
109,863✔
52
  if (point_index == 0) {
109,863✔
53
    return neighbors->start;
36,621✔
54
  } else if (point_index == 1) {
73,242✔
55
    return neighbors->goal;
36,621✔
56
  } else {
57
    return neighbors->vertices[point_index-2];
36,621✔
58
  }
59
}
60

61
Neighbors::Neighbors(const point_type_fp& start, const point_type_fp& goal,
×
62
                     const point_type_fp& current,
63
                     const coordinate_type_fp& max_path_length,
64
                     const std::vector<point_type_fp>& vertices,
65
                     const PathFindingSurface* pfs) :
36,621✔
66
    start(start),
67
    goal(goal),
68
    current(current),
69
    max_path_length_squared(max_path_length),
×
70
    vertices(vertices),
71
    pfs(pfs) {}
36,621✔
72

73
// Returns a valid neighbor index that is either the one provided or
74
// the next higher valid one.
75
inline bool Neighbors::is_neighbor(const point_type_fp p) const {
109,863✔
76
  if (p == current) {
109,863✔
77
    return false;
78
  }
79
  pfs->decrement_tries();
73,242✔
80
  if (bg::distance(current, p) + bg::distance(p, goal) > max_path_length_squared) {
36,621✔
81
    return false;
82
  }
83
  if (!pfs->in_surface(current, p)) {
18,225!
84
    return false;
18,225✔
85
  }
86
  return true;
87
}
88

89
Neighbors::iterator Neighbors::begin() const {
36,621!
90
  auto ret = iterator(this, 0);
91
  if (ret == end()) {
36,621!
92
    // Can't dereferfence the end.
93
    return ret;
×
94
  }
95
  if (is_neighbor(*ret)) {
36,621!
96
    // This is a valid begin.
97
    return ret;
×
98
  } else {
99
    // This position is invalid but we can increment to get to a valid one.
100
    ++ret;
36,621✔
101
    return ret;
×
102
  }
103
}
104

105
Neighbors::iterator Neighbors::end() const {
×
106
  return iterator(this, vertices.size()+2);
36,621!
107
}
108

109
vector<pair<point_type_fp, point_type_fp>> get_all_segments(
162✔
110
    const vector<vector<std::reference_wrapper<const ring_type_fp>>>& all_rings) {
111
  vector<pair<point_type_fp, point_type_fp>> ret;
112
  for (const auto& x0 : all_rings) {
1,850✔
113
    for (auto x1 : x0) {
3,373✔
114
      for (size_t i = 0; i+1 < x1.get().size(); i++) {
356,251✔
115
        ret.emplace_back(x1.get()[i], x1.get()[i+1]);
354,566!
116
      }
117
    }
118
  }
119
  return ret;
162✔
120
}
121

122
vector<std::reference_wrapper<const ring_type_fp>> get_all_rings(const multi_polygon_type_fp& mpolys) {
1,688✔
123
  vector<std::reference_wrapper<const ring_type_fp>> ret;
124
  for (const auto& poly : mpolys) {
3,373✔
125
    ret.push_back(poly.outer());
1,685✔
126
    for (const auto& inner : poly.inners()) {
1,685!
127
      ret.push_back(inner);
×
128
    }
129
  }
130
  return ret;
1,688✔
131
}
132

133
vector<vector<std::reference_wrapper<const ring_type_fp>>> get_all_rings(
162✔
134
    const nested_multipolygon_type_fp& mpolys) {
135
  vector<vector<std::reference_wrapper<const ring_type_fp>>> ret;
136
  for (const auto& poly : mpolys) {
847✔
137
    ret.push_back(get_all_rings(poly.outer()));
1,370!
138
    for (const auto& inner: poly.inners()) {
1,688✔
139
      ret.push_back(get_all_rings(inner));
2,006!
140
    }
141
  }
142
  return ret;
162✔
143
}
144

145
PathFindingSurface::PathFindingSurface(const optional<multi_polygon_type_fp>& keep_in,
162✔
146
                                       const multi_polygon_type_fp& keep_out,
147
                                       const coordinate_type_fp tolerance) {
324!
148
  if (keep_in) {
162✔
149
    multi_polygon_type_fp total_keep_in = *keep_in - keep_out;
132!
150

151
    total_keep_in_grown.emplace();
152
    for (const auto& poly : total_keep_in) {
460✔
153
      all_vertices.emplace_back();
328!
154
      all_vertices.back().emplace_back(poly.outer().cbegin(), poly.outer().cend());
328!
155
      total_keep_in_grown->emplace_back(bg_helpers::buffer_miter(poly.outer(), tolerance));
656!
156
      for (const auto& inner : poly.inners()) {
1,009✔
157
        all_vertices.back().emplace_back(inner.cbegin(), inner.cend());
681!
158
        // Because the inner is reversed, we need to reverse it so
159
        // that the buffer algorithm won't get confused.
160
        auto temp_inner = inner;
161
        bg::reverse(temp_inner);
162
        // tolerance needs to be inverted because growing a shape
163
        // shrinks the holes in it.
164
        total_keep_in_grown->back().inners().push_back(bg_helpers::buffer_miter(temp_inner, -tolerance));
1,362!
165
      }
166
    }
167
  } else {
168
    for (const auto& poly : keep_out){
387✔
169
      all_vertices.emplace_back();
357!
170
      all_vertices.back().emplace_back(poly.outer().cbegin(), poly.outer().cend());
357!
171
      keep_out_shrunk.emplace_back(bg_helpers::buffer_miter(poly.outer(), -tolerance));
714!
172
      for (const auto& inner : poly.inners()) {
679✔
173
        all_vertices.back().emplace_back(inner.cbegin(), inner.cend());
322!
174
        // Because the inner is reversed, we need to reverse it so
175
        // that the buffer algorithm won't get confused.
176
        auto temp_inner = inner;
177
        bg::reverse(temp_inner);
178
        // tolerance needs to be inverted because shrinking a shape
179
        // grows the holes in it.
180
        keep_out_shrunk.back().inners().push_back(bg_helpers::buffer_miter(temp_inner, tolerance));
644!
181
      }
182
    }
183
  }
184
  const nested_multipolygon_type_fp& poly_to_search = total_keep_in_grown ? *total_keep_in_grown : keep_out_shrunk;
162✔
185
  const vector<vector<std::reference_wrapper<const ring_type_fp>>>& all_rings = get_all_rings(poly_to_search);
324!
186
  const auto& all_segments = get_all_segments(all_rings);
162!
187
  segment_tree::SegmentTree x(all_segments);
162!
188
  tree = std::move(x);
189

190
  for (auto av : all_vertices) {
1,532!
191
    sort(av.begin(), av.end());
685✔
192
    av.erase(std::unique(av.begin(), av.end()), av.end());
685✔
193
  }
194
}
162✔
195

196
boost::optional<MPRingIndices> inside_multipolygon(const point_type_fp& p,
112,743✔
197
                                                   const multi_polygon_type_fp& mp) {
198
  for (size_t poly_index = 0; poly_index < mp.size(); poly_index++) {
217,756✔
199
    const auto& poly = mp[poly_index];
200
    if (point_in_ring(p, poly.outer())) {
112,743✔
201
      // Might be part of this shape but only if the point isn't in
202
      // the inners.
203
      MPRingIndices ring_indices{{poly_index, {0}}};
30,920!
204
      for (size_t inner_index = 0; inner_index < poly.inners().size(); inner_index++) {
7,730!
205
        const auto& inner = poly.inners()[inner_index];
206
        if (!point_in_ring(p, inner)) {
×
207
          // We'll have to make sure not to cross this inner.
208
          ring_indices.back().second.emplace_back(inner_index+1);
×
209
        } else {
210
          break; // We're inside one of the inners so give up.
211
        }
212
      }
213
      if (ring_indices.back().second.size() == poly.inners().size() + 1) {
7,730!
214
        // We never hit the break so we're inside this shape so we're
215
        // done.
216
        return {ring_indices};
7,730✔
217
      }
218
      // We're inside the outer but also inside an inner!  There might
219
      // be another shape inside this hold so we'll ignore this one
220
      // and keep searching.
221
    }
222
  }
223
  return boost::none;
105,013✔
224
}
225

226
boost::optional<MPRingIndices> outside_multipolygon(const point_type_fp& p,
92,978✔
227
                                                    const multi_polygon_type_fp& mp) {
228
  MPRingIndices ring_indices;
92,978✔
229
  for (size_t poly_index = 0; poly_index < mp.size(); poly_index++) {
180,152✔
230
    const auto& poly = mp[poly_index];
231
    if (point_in_ring(p, poly.outer())) {
91,370✔
232
      // We're inside the outer, maybe we're in an inner?  If not, we
233
      // aren't outside at all and we'll just give up.
234
      bool in_any_inner = false;
235
      for (size_t i = 0; i < poly.inners().size(); i++) {
4,196!
236
        const auto& inner = poly.inners()[i];
237
        if (point_in_ring(p, inner)) {
×
238
          in_any_inner = true;
239
          ring_indices.emplace_back(poly_index, vector<size_t>{i+1});
×
240
          break;
241
        }
242
      }
243
      if (!in_any_inner) {
244
        // We're inside the outer but not in any of the inners, so
245
        // we're in the shape, but we want to be outside the shape, so
246
        // we've failed.
247
        return boost::none;
4,196✔
248
      }
249
    } else {
250
      // We need to keep out of this outer.
251
      ring_indices.emplace_back(poly_index, vector<size_t>{0});
174,348!
252
      // No need to examine the inners which we can't possibly be inside.
253
    }
254
  }
255
  return {ring_indices};
256
}
257

258
boost::optional<RingIndices> inside_multipolygons(
3,760✔
259
    const point_type_fp& p,
260
    const nested_multipolygon_type_fp& mp) {
261
  for (size_t poly_index = 0; poly_index < mp.size(); poly_index++) {
82,838✔
262
    const auto& poly = mp[poly_index];
263
    boost::optional<MPRingIndices> inside_mp = inside_multipolygon(p, poly.outer());
82,699✔
264
    if (inside_mp) {
82,699✔
265
      // Might be part of this shape but only if the point isn't in
266
      // the inners.
267
      RingIndices ring_indices{{poly_index, {{0, *inside_mp}}}};
36,006!
268
      for (size_t inner_index = 0; inner_index < poly.inners().size(); inner_index++) {
44,688✔
269
        const auto& inner = poly.inners()[inner_index];
270
        auto outside_mp = outside_multipolygon(p, inner);
41,067!
271
        if (outside_mp) {
41,067✔
272
          // We'll have to make sure not to cross this inner.
273
          ring_indices.back().second.emplace_back(inner_index+1, *outside_mp);
39,027!
274
        } else {
275
          break; // We're inside one of the inners so give up.
276
        }
277
      }
278
      if (ring_indices.back().second.size() == poly.inners().size() + 1) {
5,661✔
279
        // We never hit the break so we're inside this shape so we're
280
        // done.
281
        return {ring_indices};
3,621✔
282
      }
283
      // We're inside the outer but also inside an inner!  It might
284
      // be an outer in an inner so we'll ignore this one and keep
285
      // searching.
286
    }
287
  }
288
  return boost::none;
139✔
289
}
290

291
boost::optional<RingIndices> outside_multipolygons(
1,979✔
292
    const point_type_fp& p,
293
    const nested_multipolygon_type_fp& mp) {
294
  RingIndices ring_indices;
1,979✔
295
  for (size_t poly_index = 0; poly_index < mp.size(); poly_index++) {
53,803✔
296
    const auto& poly = mp[poly_index];
297
    auto outside_mp = outside_multipolygon(p, poly.outer());
51,911!
298
    if (!outside_mp) {
51,911✔
299
      // We're inside the outer, maybe we're in an inner?  If not, we
300
      // aren't outside at all and we'll just give up.
301
      bool in_any_inner = false;
302
      for (size_t inner_index = 0; inner_index < poly.inners().size(); inner_index++) {
30,131✔
303
        const auto& inner = poly.inners()[inner_index];
304
        auto inside_mp = inside_multipolygon(p, inner);
30,044!
305
        if (inside_mp) {
30,044✔
306
          in_any_inner = true;
307
          ring_indices.emplace_back(poly_index, vector<pair<size_t, MPRingIndices>>{{inner_index + 1, *inside_mp}});
4,138!
308
          break;
309
        }
310
      }
311
      if (!in_any_inner) {
312
        // We're inside the outer but not in any of the inners, so
313
        // we're in the shape, but we want to be outside the shape, so
314
        // we've failed.
315
        return boost::none;
316
      }
317
    } else {
318
      // We need to keep out of this outer.
319
      ring_indices.emplace_back(poly_index, vector<pair<size_t, MPRingIndices>>{{0, *outside_mp}});
99,510!
320
    }
321
  }
322
  return {ring_indices};
323
}
324

325
/* Given a point, determine if the point is in the search surface.  If
326
   so, return a non-default value, otherwise default value.  If two
327
   points return the same value, there is a path between them in the
328
   surface.  If not then there cannot be a path between them.  The
329
   value will actually be a vector of size_t that indicates which
330
   rings in the stored polygon should be used for the generated points
331
   in the path and also for the collision detection. */
332
const boost::optional<SearchKey>& PathFindingSurface::in_surface(point_type_fp p) const {
58,770✔
333
  auto memoized_result = point_in_surface_memo.find(p);
334
  if (memoized_result != point_in_surface_memo.cend()) {
58,770✔
335
    return memoized_result->second;
53,031✔
336
  }
337
  boost::optional<RingIndices> maybe_ring_indices;
338
  if (total_keep_in_grown) {
5,739✔
339
    maybe_ring_indices = inside_multipolygons(p, *total_keep_in_grown);
7,520!
340
  } else {
341
    maybe_ring_indices = outside_multipolygons(p, keep_out_shrunk);
3,958!
342
  }
343
  if (!maybe_ring_indices) {
5,739✔
344
    return point_in_surface_memo.emplace(p, boost::none).first->second;
226✔
345
  }
346
  const auto& ring_indices = *maybe_ring_indices;
347
  // Check if this one is already in the cache.
348
  const auto& find_result = ring_indices_lookup.find(std::cref(ring_indices));
349
  if (find_result != ring_indices_lookup.cend()) {
5,513✔
350
    // Found in the cache so we can use that.
351
    return point_in_surface_memo.emplace(p, find_result->second).first->second;
4,897!
352
  }
353
  // Not found so we need to add it to the cache.
354
  ring_indices_cache.push_back(ring_indices);
616!
355
  ring_indices_lookup.emplace(ring_indices_cache.back(), ring_indices_cache.size()-1);
616!
356
  return point_in_surface_memo.emplace(p, ring_indices_cache.size()-1).first->second;
616!
357
}
358

359
void PathFindingSurface::decrement_tries() const {
76,334!
360
  if (tries) {
76,334!
361
    if (*tries == 0) {
76,334✔
362
      throw GiveUp();
36,621✔
363
    }
364
    (*tries)--;
39,713✔
365
  }
366
}
39,713✔
367

368
// Return true if this edge from a to b is part of the path finding surface.
369
bool PathFindingSurface::in_surface(
63,338✔
370
    const point_type_fp& a, const point_type_fp& b) const {
371
  if (b < a) {
63,338✔
372
    return in_surface(b, a);
5,400✔
373
  }
374
  const auto key = make_pair(a, b);
375
  auto memoized_result = edge_in_surface_memo.find(key);
376
  if (memoized_result != edge_in_surface_memo.cend()) {
57,938✔
377
    return memoized_result->second;
36,573✔
378
  }
379
  auto found_intersection = tree.intersects(a, b);
21,365✔
380
  edge_in_surface_memo.emplace(key, !found_intersection);
21,365✔
381
  return !found_intersection;
21,365✔
382
}
383

384
// Return all possible neighbors of current.  A neighbor can be
385
// start, end, or any of the points in all_vertices.  But only the
386
// ones that are in_surface are returned.
387
Neighbors PathFindingSurface::neighbors(const point_type_fp& start, const point_type_fp& goal,
×
388
                                        const coordinate_type_fp& max_path_length,
389
                                        SearchKey search_key,
390
                                        const point_type_fp& current) const {
391
  return Neighbors(start, goal, current, max_path_length, vertices(search_key), this);
×
392
}
393

394
// Return a path from the start to the current.  Always return at
395
// least two points.
396
linestring_type_fp build_path(
×
397
    point_type_fp current,
398
    const unordered_map<point_type_fp, point_type_fp>& came_from) {
399
  linestring_type_fp result;
400
  while (came_from.count(current)) {
×
401
    result.push_back(current);
×
402
    current = came_from.at(current);
×
403
  }
404
  result.push_back(current);
×
405
  bg::reverse(result);
406
  return result;
×
407
}
408
optional<linestring_type_fp> PathFindingSurface::find_path(
39,713✔
409
    const point_type_fp& start, const point_type_fp& goal,
410
    const coordinate_type_fp& max_path_length,
411
    SearchKey search_key) const {
412
  // Connect if a direct connection is possible.  This also takes care
413
  // of the case where start == goal.
414
  try {
415
    if (in_surface(start, goal)) {
39,713!
416
      decrement_tries();
3,092!
417
      if (bg::comparable_distance(start, goal) < max_path_length * max_path_length) {
3,092✔
418
        // in_surface builds up some structures that are only efficient if
419
        // we're doing many tries.
420
        return {{start, goal}};
4,176!
421
      } else {
422
        // If the straight line was too long then there is no way to connect.
423
        return boost::none;
1,004✔
424
      }
425
    }
426
  } catch (GiveUp g) {
×
427
    return boost::none;
428
  }
429
  // Do astar.
430
  priority_queue<pair<coordinate_type_fp, point_type_fp>,
431
                 vector<pair<coordinate_type_fp, point_type_fp>>,
432
                 std::greater<pair<coordinate_type_fp, point_type_fp>>> open_set;
433
  open_set.emplace(bg::distance(start, goal), start);
36,621!
434
  unordered_set<point_type_fp> closed_set;
435
  unordered_map<point_type_fp, point_type_fp> came_from;
436
  unordered_map<point_type_fp, coordinate_type_fp> g_score; // Empty should be considered infinity.
437
  g_score[start] = 0;
36,621✔
438
  while (!open_set.empty()) {
36,621!
439
    const auto current = open_set.top().second;
36,621✔
440
    open_set.pop();
36,621✔
441
    if (current == goal) {
442
      // We're done.
443
      return boost::make_optional(build_path(current, came_from));
×
444
    }
445
    if (closed_set.count(current) > 0) {
36,621!
446
      // Skip this because we already "removed it", sort of.
447
      continue;
×
448
    }
449
    try {
450
      const auto current_neighbors = neighbors(
451
          start, goal,
452
          max_path_length - g_score.at(current),
36,621!
453
          search_key,
454
          current);
455
      for (const auto& neighbor : current_neighbors) {
36,621!
456
        const auto tentative_g_score = g_score.at(current) + bg::distance(current, neighbor);
×
457
        if (g_score.count(neighbor) == 0 || tentative_g_score < g_score.at(neighbor)) {
×
458
          // This path to neighbor is better than any previous one.
459
          came_from[neighbor] = current;
×
460
          g_score[neighbor] = tentative_g_score;
×
461
          open_set.emplace(tentative_g_score + bg::distance(neighbor, goal), neighbor);
×
462
        }
463
      }
464
    } catch (GiveUp g) {
36,621!
465
      return boost::none;
466
    }
467
    // Because we can't delete from the open_set, we'll just marked
468
    // items as closed and ignore them later.
469
    closed_set.insert(current);
470
  }
471
  return boost::none;
×
472
}
473

474
optional<linestring_type_fp> PathFindingSurface::find_path(
34,666!
475
    const point_type_fp& start, const point_type_fp& goal,
476
    const coordinate_type_fp& max_path_length,
477
    const boost::optional<size_t>& max_tries,
478
    SearchKey search_key) const {
479
  if (max_tries) {
34,666!
480
    if (*max_tries == 0) {
34,666!
481
      return boost::none;
×
482
    }
483
    tries.emplace(*max_tries);
484
  } else {
485
    tries = boost::none;
486
  }
487
  return find_path(start, goal, max_path_length, search_key);
34,666✔
488
}
489

490
optional<linestring_type_fp> PathFindingSurface::find_path(
37,646!
491
    const point_type_fp& start, const point_type_fp& goal,
492
    const coordinate_type_fp& max_path_length,
493
    const boost::optional<size_t>& max_tries) const {
494
  if (max_tries) {
37,646!
495
    if (*max_tries == 0) {
37,646!
496
      return boost::none;
×
497
    }
498
    tries.emplace(*max_tries);
499
  } else {
500
    tries = boost::none;
501
  }
502

503
  auto ring_indices = in_surface(start);
37,646✔
504
  if (!ring_indices) {
37,646✔
505
    // Start is not in the surface.
506
    return boost::none;
18,717✔
507
  }
508
  if (ring_indices != in_surface(goal)) {
37,858✔
509
    // Either goal is not in the surface or it's in a region unreachable by start.
510
    return boost::none;
13,882✔
511
  }
512
  return find_path(start, goal, max_path_length, *ring_indices);
5,047✔
513
}
514

515
const std::vector<point_type_fp>&
516
PathFindingSurface::vertices(SearchKey search_key) const {
36,621✔
517
  auto memoized_result = vertices_memo.find(search_key);
518
  if (memoized_result != vertices_memo.cend()) {
36,621✔
519
    return memoized_result->second;
36,528✔
520
  }
521
  std::vector<point_type_fp> ret;
522
  const auto& vertices = all_vertices;
523
  const auto& ring_indices = ring_indices_cache.at(search_key);
93!
524
  for (size_t poly_index = 0; poly_index < ring_indices.size() ; poly_index++) {
791✔
525
    // This is the poly to look at.
526
    const auto& poly_ring_index = ring_indices[poly_index];
527
    // These are the vertices for that poly.
528
    const auto& poly_vertices = vertices[poly_ring_index.first];
698✔
529
    for (size_t ring_index = 0; ring_index < poly_ring_index.second.size(); ring_index++) {
1,913✔
530
      const auto& ring_ring_index = poly_ring_index.second[ring_index];
531
      const auto& ring_vertices = poly_vertices[ring_ring_index.first];
1,215!
532
      ret.insert(ret.cend(), ring_vertices.cbegin(), ring_vertices.cend());
1,215!
533
    }
534
  }
535
  return vertices_memo.emplace(search_key, ret).first->second;
93✔
536
}
537

538
} //namespace path_finding
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc