Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tiered Dwell Time with Rates #658

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions policy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ An individual `Rule` object is defined by the following fields:
| `propulsion_types` | `propulsion_type[]` | Optional | Applicable vehicle [propulsion types][propulsion-types], default "all". |
| `minimum` | integer | Optional | Minimum value, if applicable (default 0) |
| `maximum` | integer | Optional | Maximum value, if applicable (default unlimited) |
| `rate_amount` | integer | Optional | The amount of a rate applied when this rule applies, if applicable (default zero). A positive integer rate amount represents a fee, while a negative integer represents a subsidy. Rate amounts are given in the `currency` defined in the [Policy](#policy). |
| `rate_amount` | integer | Optional | Amount of the rate (see [Rate Amounts](#rate-amounts)) |
| `rate_recurrence` | enum | Optional | Recurrence of the rate (see [Rate Recurrences](#rate-recurrences)) |
| `start_time` | ISO 8601 time `hh:mm:ss` | Optional | Beginning time-of-day when the rule is in effect (default 00:00:00). |
| `end_time` | ISO 8601 time `hh:mm:ss` | Optional | Ending time-of-day when the rule is in effect (default 23:59:59). |
Expand All @@ -270,7 +270,7 @@ An individual `Rule` object is defined by the following fields:
| Name | Description |
| ------- | ------------------------------------------------------------------------------------------------------------- |
| `count` | Fleet counts based on regions. Rule `minimum`/`maximum` refers to number of devices in [Rule Units](#rule-units). |
| `time` | Individual limitations on time spent in one or more vehicle-states. Rule `minimum`/`maximum` refers to increments of time in [Rule Units](#rule-units). |
| `time` | Individual limitations or fees based upon time spent in one or more vehicle-states. Rule `minimum`/`maximum` refers to increments of time in [Rule Units](#rule-units). |
| `speed` | Global or local speed limits. Rule `minimum`/`maximum` refers to speed in [Rule Units](#rule-units). |
| `rate` | **[Beta feature](/general-information.md#beta-features):** *Yes (as of 1.0.0)*. Fees or subsidies based on regions and time spent in one or more vehicle-states. Rule `rate_amount` refers to the rate charged according to the [Rate Recurrences](#rate_recurrences) and the [currency requirements](/general-information.md#costs-and-currencies) in [Rule Units](#rule-units). *Prior to implementation agencies should consult with providers to discuss how the `rate` rule will be used. Most agencies do this as a matter of course, but it is particularly important to communicate in advance how frequently and in what ways rates might change over time.* |
| `user` | Information for users, e.g. about helmet laws. Generally can't be enforced via events and telemetry. |
Expand Down Expand Up @@ -308,15 +308,22 @@ An individual `Rule` object is defined by the following fields:

[Top][toc]

### Rate Recurrences
### Rates
Rate-related properties can currently be specified on `rate` and `time` Rules. Note: A future MDS version will likely support rates for `count` and `speed` rules, but their behavior is currently undefined.

Rate recurrences specify when a rate is applied – either once, or periodically according to a `time_unit` specified using [Rule Units](#rule-units). A `time_unit` refers to a unit of time as measured in local time for the juristiction – a day begins at midnight local time, an hour begins at the top of the hour, etc.
#### Rate Amounts
The amount of a rate applied when this rule applies, if applicable (default zero). A positive integer rate amount represents a fee, while a negative integer represents a subsidy. Rate amounts are given in the `currency` defined in the [Policy](#policy).

| Name | Description |
| --------- | ------------------- |
| `once` | Rate is applied once to vehicles entering a matching status from a non-matching status. |
| `each_time_unit` | During each `time_unit`, rate is applied once to vehicles entering or remaining in a matching status. Requires a `time_unit` to be specified using `rule_units`. |
| `per_complete_time_unit` | Rate is applied once per complete `time_unit` that vehicles remain in a matching status. Requires a `time_unit` to be specified using `rule_units`. |
#### Rate Recurrences

Rate recurrences specify when a rate is applied – either once, or periodically according to a `time_unit` specified using [Rule Units](#rule-units). A `time_unit` refers to a unit of time as measured in local time for the jurisdiction – a day begins at midnight local time, an hour begins at the top of the hour, etc.

| Name | Description |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `once_on_match` | Rate is applied once when a vehicle transitions **into** a matching status from a non-matching status. |
| `once_on_unmatch` | Rate is applied once a vehicle transitions **out of** a matching status to a non-matching status. |
| `each_time_unit` | During each `time_unit`, rate is applied once to vehicles entering or remaining in a matching status. Requires a `time_unit` to be specified using `rule_units`. |
| `per_complete_time_unit` | Rate is applied once per complete `time_unit` that vehicles remain in a matching status. Requires a `time_unit` to be specified using `rule_units`. |

[Top][toc]

Expand Down
139 changes: 131 additions & 8 deletions policy/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ This file presents a series of example [Policy documents](../README.md#schema) f

## Table of Contents

- [Prohibited Zone](#prohibited-zone)
- [Provider Cap](#provider-cap)
- [Idle Time](#idle-time)
- [Speed Limits](#speed-limits)
- [Per Trip Fees](#per-trip-fees)
- [Vehicle Right of Way Fees](#vehicle-right-of-way-fees)
- [Metered Parking Fees](#metered-parking-fees)
- [Required Parking](#required-parking)
- [Policy Examples](#policy-examples)
- [Table of Contents](#table-of-contents)
- [Prohibited Zone](#prohibited-zone)
- [Provider Cap](#provider-cap)
- [Idle Time](#idle-time)
- [Speed Limits](#speed-limits)
- [Per Trip Fees](#per-trip-fees)
- [Vehicle Right of Way Fees](#vehicle-right-of-way-fees)
- [Metered Parking Fees](#metered-parking-fees)
- [Required Parking](#required-parking)
- [Tiered Parking Fees Per Hour](#tiered-parking-fees-per-hour)
- [Tiered Parking Fees Total](#tiered-parking-fees-total)

## Prohibited Zone

Expand Down Expand Up @@ -445,5 +449,124 @@ File: [`required-parking.json`](required-parking.json)
}
```

## Tiered Parking Fees Per Hour
This policy states parking fees as such:
- Parking for the first hour costs $2
- Parking for the second hour costs $4
- Parking every hour onwards costs $10

For example, say a vehicle is parked for 6.5 hours. It will be charged `$2 (0-1hr) + $4 (1-2hr) + $10 (2-3hr) + $10 (3-4hr) + $10 (4-5hr) + $10 (5-6hr) + $10 (6-6.5hr) = $56`
File: [`tiered-parking-fees-per-hour.json`](tiered-parking-fees-per-hour.json)
```
{
"name": "Tiered Dwell Time Example",
"description": "First hour $2, second hour $4, every hour onwards $10",
"policy_id": "2800cd0a-7827-4110-9713-b9e5bf29e9a1",
"start_date": 1558389669540,
"publish_date": 1558389669540,
"end_date": null,
"prev_policies": null,
"provider_ids": [],
"currency": "USD",
"rules": [
{
"name": "> 2 hours",
"rule_id": "9cd1768c-ab9e-484c-93f8-72a7078aa7b9",
"rule_type": "time",
"rule_units": "hours",
"geographies": ["0c77c813-bece-4e8a-84fd-f99af777d198"],
"statuses": { "available": [], "non_operational": [] },
"vehicle_types": ["bicycle", "scooter"],
"maximum": 2,
"rate_amount": 1000,
"rate_recurrence": "each_time_unit"
},
{
"name": "1-2 Hours",
"rule_id": "edd6a195-bb30-4eb5-a2cc-44e5a18798a2",
"rule_type": "time",
"rule_units": "hours",
"geographies": ["0c77c813-bece-4e8a-84fd-f99af777d198"],
"statuses": { "available": [], "non_operational": [] },
"vehicle_types": ["bicycle", "scooter"],
"maximum": 1,
"rate_amount": 400,
"rate_recurrence": "each_time_unit"
},
{
"name": "0-1 Hour",
"rule_id": "6b6fe61b-dbe5-4367-8e35-84fb14d23c54",
"rule_type": "time",
"rule_units": "hours",
"geographies": ["0c77c813-bece-4e8a-84fd-f99af777d198"],
"statuses": { "available": [], "non_operational": [] },
"vehicle_types": ["bicycle", "scooter"],
"maximum": 0,
"rate_amount": 200,
"rate_recurrence": "each_time_unit"
}
]
}
```

## Tiered Parking Fees Total
This policy states parking fees as such:
- If parked for less than an hour, $2 on exit
- If parked for less than 2 hours, $4 on exit
- If parked for any duration longer than 2 hours, $10 on exit

For example, if a vehicle is parked for 6.5 hours, it will be charged $10 on exit.
File: [`tiered-parking-fees-total.json`](tiered-parking-fees-total.json)
```
{
"name": "Tiered Dwell Time Example",
"description": "If parked for <1hr $2 upon exit, if parked for 1-2 hours $4 upon exit, if parked for longer than 2 hours $10 upon exit",
"policy_id": "2800cd0a-7827-4110-9713-b9e5bf29e9a1",
"start_date": 1558389669540,
"publish_date": 1558389669540,
"end_date": null,
"prev_policies": null,
"provider_ids": [],
"currency": "USD",
"rules": [
{
"name": "> 2 hours",
"rule_id": "9cd1768c-ab9e-484c-93f8-72a7078aa7b9",
"rule_type": "time",
"rule_units": "hours",
"geographies": ["0c77c813-bece-4e8a-84fd-f99af777d198"],
"statuses": { "available": [], "non_operational": [] },
"vehicle_types": ["bicycle", "scooter"],
"maximum": 2,
"rate_amount": 1000,
"rate_recurrence": "once_on_unmatch"
},
{
"name": "1-2 Hours",
"rule_id": "edd6a195-bb30-4eb5-a2cc-44e5a18798a2",
"rule_type": "time",
"rule_units": "hours",
"geographies": ["0c77c813-bece-4e8a-84fd-f99af777d198"],
"statuses": { "available": [], "non_operational": [] },
"vehicle_types": ["bicycle", "scooter"],
"maximum": 1,
"rate_amount": 400,
"rate_recurrence": "once_on_unmatch"
},
{
"name": "0-1 Hour",
"rule_id": "6b6fe61b-dbe5-4367-8e35-84fb14d23c54",
"rule_type": "time",
"rule_units": "hours",
"geographies": ["0c77c813-bece-4e8a-84fd-f99af777d198"],
"statuses": { "available": [], "non_operational": [] },
"vehicle_types": ["bicycle", "scooter"],
"maximum": 0,
"rate_amount": 200,
"rate_recurrence": "once_on_unmatch"
}
]
}
```
[Top](#table-of-contents)

57 changes: 57 additions & 0 deletions policy/examples/tiered-parking-fees-per-hour.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"updated": 0,
"version": "1.2.0",
"data": {
"policy": [
{
"name": "Tiered Dwell Time Example",
"description": "First hour $2, second hour $4, every hour onwards $10",
"policy_id": "2800cd0a-7827-4110-9713-b9e5bf29e9a1",
"start_date": 1558389669540,
"publish_date": 1558389669540,
"end_date": null,
"prev_policies": null,
"provider_ids": [],
"currency": "USD",
"rules": [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to make sure I understand the process for interpreting these rules, so I'm going to walk through an example. Let's say we have a 6.1 hour long parking event. The total charge for that under this policy as I understand it would be 2 + 4 + 10 * 5 = $56, right?

So the excruciatingly detailed process for calculating that would be:

  1. Iterate over rules
  2. Looking at the first rule:
    1. This is an each_time_unit rule, so I need to figure out how many units of time to apply it to
    2. The rule is in units of hours, so I make sure my duration is also in hours, or in some other way normalize the units of the rule and my event
    3. This rule has a maximum of 2 and my event is longer than that, so this rule applies to my event
    4. This rule has a maximum of 2 so I subtract 2 from 6.1 to get 4.1 hours of this event this rule applies to
      • I don't understand how the word maximum describes these last two operations, hoping for some clarification on that
    5. each_time_unit rules are always applied to whole time units, never fractional, so I round 4.1 up to 5 to get the total number of hours used to calculate this portion of the charge
      • Is this assumption written down anywhere?
    6. The rate_amount for this rule is 1000, I multiple 5 hours * 1000 = 5000 cents = $50
    7. Having accounted for this portion of the parking event, I only carry forward the portion of this parking event that is less than the maximum field, meaning the first 2 hours. I replace my original parking event with one that is 2 hours long as I head to the next rule. This is standard practice for each_time_unit rules.
  3. Looking at the second rule now that I have a 2 hour parking event:
    1. This is an each_time_unit rule, so I need to figure out how many units of time to apply it to
    2. The rule is in units of hours, so I make sure my duration is also in hours, or in some other way normalize the units of the rule and my event
    3. This rule has a maximum of 1 and my event is longer than that, so this rule applies to my event
    4. This rule has a maximum of 1 so I subtract 1 from 2 to get 1 hour of this event this rule applies to
    5. each_time_unit rules are always applied to whole time units, never fractional, so I round 1 up to 1 to get the total number of hours used to calculate this portion of the charge (hoping there's no floating point issues causing the rounding to go to 2)
    6. The rate_amount for this rule is 400, I multiple 1 hour * 400 = 400 cents = $4
    7. Having accounted for this portion of the parking event, I only carry forward the portion of this parking event that is less than the maximum field, meaning the first 1 hour. I replace my original parking event with one that is 1 hour long as I head to the next rule.
  4. Looking at the third rule now that I have a 1 hour parking event:
    1. This is an each_time_unit rule, so I need to figure out how many units of time to apply it to
    2. The rule is in units of hours, so I make sure my duration is also in hours, or in some other way normalize the units of the rule and my event
    3. This rule has a maximum of 0 and my event is longer than that, so this rule applies to my event
    4. This rule has a maximum of 0 so I subtract 0 from 1 to get 1 hour of this event this rule applies to
    5. each_time_unit rules are always applied to whole time units, never fractional, so I round 1 up to 1 to get the total number of hours used to calculate this portion of the charge (hoping there's no floating point issues causing the rounding to go to 2)
    6. The rate_amount for this rule is 200, I multiple 1 hour * 200 = 200 cents = $2
    7. Having accounted for this portion of the parking event, I only carry forward the portion of this parking event that is less than the maximum field, meaning 0 hours.
    8. Having applied rules to the entirety of my parking event (left with one 0 hours long) I know that no more rules will apply to my event and I can break out of the iteration over rules.
      • Is this true? Could there be another rule after this one with a flat fee or something?
  5. I sum up the charges from each rule and get $56 total

Does that seem right? If one of these rules had a minimum field, how would that factor in?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the methodology you described functionally works how I'd expect at the end of the day, and the logic checks out! However, I have perhaps a slightly more intuitive way of thinking about it. The most intuitive way I can think of evaluating policies with rates that specify each_time_unit, is by treating each_time_unit tick as an event (note, not an MDS event, more of an event-driven-system kind of event). This would enable continuous charging throughout the duration of the parking, as opposed to waiting till the absolute duration of the park has been completed.

Let's, for the sake of making this a bit easier to reason about, assume that the time_unit for each rule is the same, so we can evaluate the whole policy at once every hour on the dot. Because this is each_time_unit instead of each_complete_time_unit, we will evaluate on first entry in addition to after each hour ticks.

Continuous(ish) Evaluation Methodology for this Policy

I'm gonna approach this with some brevity here, but if you'd like me to dial into some more detail please let me know and I'm happy to!

@00:00 (right when first parked)

  • First rule doesn't match, continue
  • Second rule doesn't match, continue
  • Third rule matches, charge $2

@01:00

  • First rule doesn't match, continue
  • Second rule matches, charge $4, short-circuit execution

@02:00

  • First rule matches, charge $10, short-circuit execution

@03:00

  • First rule matches, charge $10, short-circuit execution

@04:00

  • First rule matches, charge $10, short-circuit execution

@05:00

  • First rule matches, charge $10, short-circuit execution

@06:00

  • First rule matches, charge $10, short-circuit execution

At the end of the day, there will be a charge for $2 + $4 + $10 + $10 + $10 + $10 + $10 = $56.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order for this to work the the comparison to maximum looks like it needs to be >=? And same for minimum?

If you did have different units in the rules would you pre-process the rules into the same units in order to figure out your clock-ticks?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, >= for maximums, and similarly <= for minimums I think (let me update my other comment, I think I didn't include the = component).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I had different units on the rules I'd probably specify an interval to re-evaluate of the lowest-duration unit referenced (e.g. if there were some rules with minutes and some with hours, I'd evaluate on a minutely basis). I'd then probably add some logic to ensure that double-fees won't get charged if a high-duration rule is hit again within that unit of its last occurrence, and instead results in a short-circuit without a charge.

if (current_timestamp - prev_fee_timestamp < matched_rule.unit) {
  // don't charge anything, because we don't want to double charge
}

{
"name": "> 2 hours",
"rule_id": "9cd1768c-ab9e-484c-93f8-72a7078aa7b9",
"rule_type": "time",
"rule_units": "hours",
"geographies": ["0c77c813-bece-4e8a-84fd-f99af777d198"],
"statuses": { "available": [], "non_operational": [] },
"vehicle_types": ["bicycle", "scooter"],
"maximum": 2,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain how maximum is interpreted here? And conversely, how does one interpret minimum? I know we talked about this on a call and I think I kind of understood it then, but I've forgotten now... 😬 I think this needs some clarifying documentation or a more clear name.

Copy link
Contributor Author

@avatarneil avatarneil Jul 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maximum in this case would be "If a vehicle surpasses this maximum, it's matched by this rule". Similarly, a minimum would be "If a vehicle is under this minimum, it's matched by this rule". I can't think of any good real minimum time policies off the top of my head, but essentially you could encode something like "Scooters must be parked here in state x for a minimum of 1 hour prior to doing anything else". Maybe this could be used to encode some rules around off-hours usage perhaps?

You can think of the matching along the lines of:

if (time_since_last_event >= maximum) {
  // maybe a match, check other criteria (geos, states, etc...)
} else if (time_since_last_event =< minimum) {
  // maybe a match, check other criteria (geos, states, etc...)
} else {
  definitely not a match
}

"rate_amount": 1000,
"rate_recurrence": "each_time_unit"
},
{
"name": "1-2 Hours",
"rule_id": "edd6a195-bb30-4eb5-a2cc-44e5a18798a2",
"rule_type": "time",
"rule_units": "hours",
"geographies": ["0c77c813-bece-4e8a-84fd-f99af777d198"],
"statuses": { "available": [], "non_operational": [] },
"vehicle_types": ["bicycle", "scooter"],
"maximum": 1,
"rate_amount": 400,
"rate_recurrence": "each_time_unit"
},
{
"name": "0-1 Hour",
"rule_id": "6b6fe61b-dbe5-4367-8e35-84fb14d23c54",
"rule_type": "time",
"rule_units": "hours",
"geographies": ["0c77c813-bece-4e8a-84fd-f99af777d198"],
"statuses": { "available": [], "non_operational": [] },
"vehicle_types": ["bicycle", "scooter"],
"maximum": 0,
"rate_amount": 200,
"rate_recurrence": "each_time_unit"
}
]
}
]
}
}
57 changes: 57 additions & 0 deletions policy/examples/tiered-parking-fees-total.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"updated": 0,
"version": "1.2.0",
"data": {
"policy": [
{
"name": "Tiered Dwell Time Example",
"description": "If parked for <1hr $2 upon exit, if parked for 1-2 hours $4 upon exit, if parked for longer than 2 hours $10 upon exit",
"policy_id": "2800cd0a-7827-4110-9713-b9e5bf29e9a1",
"start_date": 1558389669540,
"publish_date": 1558389669540,
"end_date": null,
"prev_policies": null,
"provider_ids": [],
"currency": "USD",
"rules": [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to make sure I understand the process for interpreting these rules, so I'm going to walk through an example. Let's say we have a 1.25 hour long parking event. The total charge for that under this policy would be $4.

So the excruciatingly detailed process for calculating that would be:

  1. Iterate over rules
  2. Looking at the first rule:
    1. This is a once_on_unmatch rule so I need to figure out if I have parking event that has gone from matching this rule to not matching this rule
    2. This rule has rule_units of hours and I normalize my event duration with the rule's units
    3. The rule has a maximum of 2, which is more than the duration of my event, so I move on to the next rule
  3. Looking at the second rule:
    1. This is a once_on_unmatch rule so I need to figure out if I have parking event that has gone from matching this rule to not matching this rule
    2. This rule has rule_units of hours and I normalize my event duration with the rule's units
    3. This rule has a maximum of 1. A once_on_unmatch rule always applies when my event is longer than a rule's maximum, so I need to use this rule to calculate a fee.
    4. The fee for this rule is 400 = $4
    5. once_on_unmatch rules always apply to the entirety of a parking event and once I've applied rules to my entire event I can stop iterating over rules, even if there are more, so I break out of the loop over rules.
      • Is this true? Could there be more applicable rules, like rate rules? Or would those be in a different policy?
  4. I sum up the charges for each rule for a total of $4

Does that all seem right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All sounds right to me! Short-circuiting should behave exactly how you described.

{
"name": "> 2 hours",
"rule_id": "9cd1768c-ab9e-484c-93f8-72a7078aa7b9",
"rule_type": "time",
"rule_units": "hours",
"geographies": ["0c77c813-bece-4e8a-84fd-f99af777d198"],
"statuses": { "available": [], "non_operational": [] },
"vehicle_types": ["bicycle", "scooter"],
"maximum": 2,
"rate_amount": 1000,
"rate_recurrence": "once_on_unmatch"
},
{
"name": "1-2 Hours",
"rule_id": "edd6a195-bb30-4eb5-a2cc-44e5a18798a2",
"rule_type": "time",
"rule_units": "hours",
"geographies": ["0c77c813-bece-4e8a-84fd-f99af777d198"],
"statuses": { "available": [], "non_operational": [] },
"vehicle_types": ["bicycle", "scooter"],
"maximum": 1,
"rate_amount": 400,
"rate_recurrence": "once_on_unmatch"
},
{
"name": "0-1 Hour",
"rule_id": "6b6fe61b-dbe5-4367-8e35-84fb14d23c54",
"rule_type": "time",
"rule_units": "hours",
"geographies": ["0c77c813-bece-4e8a-84fd-f99af777d198"],
"statuses": { "available": [], "non_operational": [] },
"vehicle_types": ["bicycle", "scooter"],
"maximum": 0,
"rate_amount": 200,
"rate_recurrence": "once_on_unmatch"
}
]
}
]
}
}