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

Scala-Robotics-Simulator / PPS-22-srs / #418

28 Aug 2025 09:45AM UTC coverage: 46.722% (+0.7%) from 46.066%
#418

Pull #67

github

davidcohenDC
refactor(obstacleAvoidance): code formatting and import statements
Pull Request #67: feat(obstacleAvoidance): implement obstacle avoidance behavior in policy

53 of 66 new or added lines in 8 files covered. (80.3%)

2 existing lines in 1 file now uncovered.

1247 of 2669 relevant lines covered (46.72%)

6.6 hits per line

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

81.48
/src/main/scala/io/github/srs/model/entity/dynamicentity/sensor/Sensor.scala
1
package io.github.srs.model.entity.dynamicentity.sensor
2

3
import cats.syntax.all.*
4
import io.github.srs.model.PositiveDouble
5
import io.github.srs.model.entity.dynamicentity.{ DynamicEntity, Robot }
6
import io.github.srs.model.entity.{ Orientation, Point2D }
7
import io.github.srs.model.environment.Environment
8
import io.github.srs.model.validation.Validation
9
import io.github.srs.utils.Ray.intersectRay
10
import io.github.srs.utils.SimulationDefaults.DynamicEntity.Sensor.ProximitySensor as ProximitySensorDefaults
11
import io.github.srs.model.illumination.model.ScaleFactor
12
import cats.Monad
13
import io.github.srs.model.entity.ShapeType
14

15
/**
16
 * Represents the range of a sensor.
17
 */
18
type Range = Double
19

×
20
/**
21
 * Represents a sensor that can sense the environment for a dynamic entity.
22
 * @tparam Entity
23
 *   the type of dynamic entity that the sensor can act upon.
24
 * @tparam Env
25
 *   the type of environment in which the sensor operates.
26
 */
27
trait Sensor[-Entity <: DynamicEntity, -Env <: Environment]:
28
  /**
8✔
29
   * The type of data that the sensor returns. This type can vary based on the specific sensor implementation.
30
   */
31
  type Data
32

33
  /**
34
   * The offset orientation of the sensor relative to the entity's orientation.
35
   * @return
36
   *   the orientation offset of the sensor.
37
   */
38
  def offset: Orientation
39

40
  /**
41
   * Senses the environment for the given entity and returns the data collected by the sensor.
42
   * @param entity
43
   *   the dynamic entity that the sensor is attached to.
44
   * @param env
45
   *   the environment in which the sensor operates.
46
   * @tparam F
47
   *   the effect type in which the sensing operation is performed.
48
   * @return
49
   *   a monadic effect containing the data sensed by the sensor.
50
   */
51
  def sense[F[_]: Monad](entity: Entity, env: Env): F[Data]
52

53
  private[sensor] def direction(entity: Entity): Point2D =
54
    val globalOrientation = entity.orientation.toRadians + offset.toRadians
4✔
55
    (math.cos(globalOrientation), math.sin(globalOrientation))
24✔
56

19✔
57
  private[sensor] def origin(entity: Entity): Point2D =
58
    import Point2D.*
4✔
59
    val distance = entity.shape match
60
      case ShapeType.Circle(radius) => radius
7✔
61
      case ShapeType.Rectangle(width, height) => math.min(width, height)
19✔
62
    val origin = entity.position + direction(entity) * distance
1✔
63
    origin
33✔
64
end Sensor
2✔
65

66
/**
67
 * Represents a reading from a sensor. This case class encapsulates the sensor and the value it has sensed.
68
 * @param sensor
69
 *   the sensor that has taken the reading.
70
 * @param value
71
 *   the value sensed by the sensor.
72
 * @tparam S
73
 *   the type of sensor, which is a subtype of [[Sensor]].
74
 * @tparam A
75
 *   the type of data sensed by the sensor.
76
 */
77
final case class SensorReading[S <: Sensor[?, ?], A](sensor: S, value: A)
78

68✔
79
/**
80
 * A collection of sensor readings. This type is used to represent multiple sensor readings from a dynamic entity. It is
81
 * a vector of [[SensorReading]] instances, allowing for efficient access and manipulation of sensor data.
82
 */
83
type SensorReadings = Vector[SensorReading[? <: Sensor[?, ?], ?]]
84

85
/**
86
 * A collection of proximity sensor readings. This type is a specialized version of [[SensorReadings]], specifically for
87
 * proximity sensors. It is a vector of [[SensorReading]] instances where the sensor is a [[ProximitySensor]] and the
88
 * data is of type `Double`.
89
 */
90
type ProximityReadings = Vector[SensorReading[ProximitySensor[?, ?], ProximitySensor[?, ?]#Data]]
91

92
/**
93
 * A collection of light sensor readings. This type is a specialized version of [[SensorReadings]], specifically for
94
 * light sensors. It is a vector of [[SensorReading]] instances where the sensor is a [[LightSensor]] and the data is of
95
 * type `Double`.
96
 */
97
type LightReadings = Vector[SensorReading[LightSensor[?, ?], LightSensor[?, ?]#Data]]
98

99
object SensorReadings:
100

3✔
101
  extension (readings: SensorReadings)
102

5✔
103
    /**
104
     * Pretty prints the sensor readings in a human-readable format. Each reading is formatted based on the type of
105
     * sensor
106
     *
107
     * @return
108
     *   a vector of strings representing the pretty-printed sensor readings.
109
     */
110
    def prettyPrint: Vector[String] =
111
      readings
×
112
        .map(r =>
×
113
          r.sensor match
×
114
            case ProximitySensor(offset, range) =>
115
              s"Proximity (offset: ${offset.degrees}°, range: $range m) -> ${r.value}"
116
            case LightSensor(offset) => s"Light (offset: ${offset.degrees}°) -> ${r.value}"
117
            case other => s"${other.getClass.getSimpleName} -> ${r.value}",
118
        )
119

×
120
    /**
121
     * Filters the sensor readings to include only those from proximity sensors.
122
     *
123
     * @return
124
     *   a vector of sensor readings from proximity sensors.
125
     */
126
    def proximityReadings: ProximityReadings =
127
      readings.collect { case _ @SensorReading(s: ProximitySensor[?, ?], v: Double) =>
4✔
128
        SensorReading(s, v)
33✔
129
      }
15✔
130

3✔
131
    /**
132
     * Filters the sensor readings to include only those from light sensors.
133
     *
134
     * @return
135
     *   a vector of sensor readings from light sensors.
136
     */
137
    def lightReadings: LightReadings =
NEW
138
      readings.collect { case _ @SensorReading(s: LightSensor[?, ?], v: Double) =>
×
NEW
139
        SensorReading(s, v)
×
NEW
140
      }
×
NEW
141
  end extension
×
142

143
end SensorReadings
144

145
/**
146
 * A proximity sensor that can sense the distance to other entities in the environment. It calculates the distance to
147
 * the nearest entity within its range and returns a normalized value. The value is normalized to a range between 0.0
148
 * (closest) and 1.0 (farthest).
149
 * @param offset
150
 *   the offset orientation of the sensor relative to the entity's orientation.
151
 * @param distance
152
 *   the distance from the center of the entity to the sensor.
153
 * @param range
154
 *   the range of the sensor, which defines how far it can sense.
155
 * @tparam Entity
156
 *   the type of dynamic entity that the sensor can act upon.
157
 * @tparam Env
158
 *   the type of environment in which the sensor operates.
159
 */
160
final case class ProximitySensor[Entity <: DynamicEntity, Env <: Environment](
161
    override val offset: Orientation = Orientation(ProximitySensorDefaults.defaultOffset),
72✔
162
    val range: Range = ProximitySensorDefaults.defaultRange,
3✔
163
) extends Sensor[Entity, Env]:
3✔
164

165
  override type Data = Double
166

167
  private def rayEnd(entity: Entity): Point2D =
168
    import Point2D.*
4✔
169
    origin(entity) + direction(entity) * range
170

35✔
171
  override def sense[F[_]: Monad](entity: Entity, env: Env): F[Data] =
172
    Monad[F].pure:
4✔
173
      val o = origin(entity)
10✔
174
      val end = rayEnd(entity)
12✔
175

8✔
176
      val distances = env.entities
177
        .filter(!_.equals(entity))
5✔
178
        .flatMap(intersectRay(_, o, end))
7✔
179
        .filter(_ <= range)
8✔
180

8✔
181
      distances.minOption.map(_ / range).getOrElse(1.0)
182

29✔
183
end ProximitySensor
184

185
/**
186
 * A light sensor that senses the light intensity in the environment.
187
 * @param offset
188
 *   the offset orientation of the sensor relative to the entity's orientation.
189
 * @param distance
190
 *   the distance from the center of the entity to the sensor.
191
 * @param range
192
 *   the range of the sensor, which defines how far it can sense.
193
 * @tparam Entity
194
 *   the type of dynamic entity that the sensor can act upon.
195
 * @tparam Env
196
 *   the type of environment in which the sensor operates.
197
 */
198
final case class LightSensor[Entity <: DynamicEntity, Env <: Environment](
199
    offset: Orientation = Orientation(ProximitySensorDefaults.defaultOffset),
58✔
200
) extends Sensor[Entity, Env]:
6✔
201

202
  override type Data = Double
203

204
  /**
205
   * Senses the light intensity at the position of the sensor in the environment. It uses a cached light map to compute
206
   * the field of light and samples it at the sensor's position.
207
   * @param entity
208
   *   the dynamic entity that the sensor is attached to.
209
   * @param env
210
   *   the environment in which the sensor operates.
211
   * @return
212
   *   a monadic effect containing the light intensity sensed by the sensor.
213
   */
214
  override def sense[F[_]: Monad](entity: Entity, env: Env): F[Data] =
215
    Monad[F].pure:
4✔
216
      val o = origin(entity)
10✔
217
      env.lightField.illuminationAt(o)(using ScaleFactor.default)
12✔
218

20✔
219
end LightSensor
220

221
object Sensor:
222

×
223
  extension [E <: DynamicEntity, Env <: Environment](s: ProximitySensor[E, Env])
224

5✔
225
    /**
226
     * Validates the properties of a sensor.
227
     */
228
    def validate: Validation[ProximitySensor[E, Env]] =
229
      for _ <- PositiveDouble(s.range).validate
4✔
230
      yield s
21✔
231

3✔
232
  extension (r: Robot)
233

234
    /**
235
     * Senses all sensors of the robot in the given environment.
236
     * @param env
237
     *   the environment in which to sense.
238
     * @return
239
     *   a vector of sensor readings.
240
     */
241
    def senseAll[F[_]: Monad](env: Environment): F[SensorReadings] =
242
      r.sensors.traverse: sensor =>
4✔
243
        sensor.sense(r, env).map(reading => SensorReading(sensor, reading))
17✔
244
end Sensor
6✔
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