Patent Expiration Dates#
Let’s see how we can estimate utility patent expiration dates using patent_client. The relevant law describes the expiration date as:
35 U.S.C. 154(a)(2)-(3)
(2)Term.— Subject to the payment of fees under this title, such grant shall be for a term beginning on the date on which the patent issues and ending 20 years from the date on which the application for the patent was filed in the United States or, if the application contains a specific reference to an earlier filed application or applications under section 120, 121, 365(c), or 386(c), from the date on which the earliest such application was filed.
(3)Priority.— Priority under section 119, 365(a), 365(b), 386(a), or 386(b) shall not be taken into account in determining the term of a patent.
To unpack that a bit, the default term is 20 years from the earliest of:
the patent’s filing date
the filing date of any continuation or continuation-in-part (section 120), divisional (section 121), or
a US Application claiming priority to a PCT patent or Hague Convention design patent application (365(b) / 386(b))
From there, there can be extensions and limits, but we’ll address that after we can figure out the default term.
We’ll use Tesla’s patents as an example here. First, let’s fetch their portfolio, and then filter out cases that dont have a patent number, and that aren’t design patents (which have a different rule):
[1]:
import pandas as pd
from patent_client import USApplication, Assignment
company_name = 'Tesla Motors'
applicant_apps = USApplication.objects.filter(first_named_applicant=company_name)
patents = list(set(a for a in applicant_apps if a.patent_number is not None and a.app_type == 'Utility'))# Selects only cases that have a patent number and deduplicates
len(patents)
[1]:
121
Default Patent Term#
To figure out 20 years from their original filing date is pretty easy, so let’s do that. We’ll pull in another Python library called dateutil that has an easy-to-use class called “relativedelta” that’s great for these kinds of calculations:
[2]:
from dateutil.relativedelta import relativedelta
default_expiry = pd.DataFrame(data={
'application_number': [a.appl_id for a in patents],
'filing_date': [a.app_filing_date for a in patents],
'20_years_from_filing': [a.app_filing_date + relativedelta(years=20) for a in patents]
}, index=[a.patent_number for a in patents])
default_expiry.head()
[2]:
application_number | filing_date | 20_years_from_filing | |
---|---|---|---|
8428806 | 13675715 | 2012-11-13 | 2032-11-13 |
9975508 | 14522352 | 2014-10-23 | 2034-10-23 |
9103143 | 13626864 | 2012-09-25 | 2032-09-25 |
9761919 | 14189219 | 2014-02-25 | 2034-02-25 |
9045030 | 14168351 | 2014-01-30 | 2034-01-30 |
But that isn’t quite what we want. We want to look at all the parent cases for each application, and see if there are parent cases that fall into one of the categories mentioned above. We know that we don’t need to worry about the foreign priority problems, because USApplications stores foreign priority data (which is irrelevant) in the USApplication.foreign_priority attribute. But what is in “parents”?
Let’s first take a peek at what all the relationship data looks like for all the cases by collecting all the parents, and then looking at all non-duplicate relationship types:
[3]:
parents = list()
for p in patents:
parents += p.parent_continuity
relationship_types = list(set(p.relationship for p in parents))
for t in relationship_types:
print(t)
is a Continuation of
Claims Priority from Provisional Application
is National Stage Entry of
is a Continuation in part of
is a Division of
From this, we see the only things we want to exclude are Provisional Applications (filed under section 119), and reissue applications (filed under section 251). The rest fall squarely into the categories listed above. So, let’s write a simple function that takes an application and figures out the earliest priority date that isn’t a Provisional Application or a Reissue, and then apply it to create a new DataFrame.
[4]:
class Relationship():
pass
def earliest_term_parent(app):
parents = app.parent_continuity
term_parents = [p for p in parents
if p.relationship not in [
'Claims Priority from Provisional Application',
'is a Reissue of'
]]
if not term_parents:
# Create a dummy relationship if there are no matching relationships
earliest_parent = Relationship()
earliest_parent.parent_app_filing_date = app.app_filing_date
earliest_parent.relationship = 'Self'
earliest_parent.parent_appl_id = app.appl_id
return earliest_parent
earliest_parent = sorted(term_parents, key=lambda x: x.parent_app_filing_date)[0]
return earliest_parent
earliest_parents = [earliest_term_parent(p) for p in patents]
default_expiry = pd.DataFrame(data={
'application_number': [a.appl_id for a in patents],
'filing_date': [a.app_filing_date for a in patents],
'number_of_parents': [len(p.parent_continuity) for p in patents],
'earliest_parent_case': [p.parent_appl_id for p in earliest_parents],
'earliest_parent_relationship': [p.relationship for p in earliest_parents],
'earliest_parent_filing_date': [p.parent_app_filing_date for p in earliest_parents],
'20_years_from_filing': [p.parent_app_filing_date + relativedelta(years=20) for p in earliest_parents],
}, index=[a.patent_number for a in patents])
default_expiry.head()
[4]:
application_number | filing_date | number_of_parents | earliest_parent_case | earliest_parent_relationship | earliest_parent_filing_date | 20_years_from_filing | |
---|---|---|---|---|---|---|---|
8428806 | 13675715 | 2012-11-13 | 4 | PCT/US08/77842 | is National Stage Entry of | 2008-09-26 | 2028-09-26 |
9975508 | 14522352 | 2014-10-23 | 1 | 13872866 | is a Division of | 2013-04-29 | 2033-04-29 |
9103143 | 13626864 | 2012-09-25 | 4 | 13626864 | Self | 2012-09-25 | 2032-09-25 |
9761919 | 14189219 | 2014-02-25 | 0 | 14189219 | Self | 2014-02-25 | 2034-02-25 |
9045030 | 14168351 | 2014-01-30 | 2 | 13308300 | is a Continuation of | 2011-11-30 | 2031-11-30 |
As a sanity check, let’s grab the case with the largest number of parents, and make sure the right one was picked:
[5]:
row = default_expiry.sort_values('number_of_parents', ascending=False).iloc[0]
print(row)
app_id = row['application_number']
app = next(p for p in patents if p.appl_id == app_id)
pd.DataFrame.from_records(r.to_dict() for r in app.parent_continuity).sort_values('parent_app_filing_date')
application_number 15384723
filing_date 2016-12-20
number_of_parents 6
earliest_parent_case 12322218
earliest_parent_relationship is a Continuation in part of
earliest_parent_filing_date 2009-01-29
20_years_from_filing 2029-01-29
Name: 10131248, dtype: object
[5]:
child_appl_id | parent_app_filing_date | parent_app_status | parent_appl_id | relationship | |
---|---|---|---|---|---|
4 | 15384723 | 2009-01-29 | Abandoned | 12322218 | is a Continuation in part of |
5 | 15384723 | 2009-02-26 | Patented | 12380427 | is a Continuation in part of |
3 | 15384723 | 2010-05-18 | Patented | 12782413 | is a Division of |
2 | 15384723 | 2013-04-19 | Patented | 13866214 | is a Continuation of |
1 | 15384723 | 2014-05-19 | Patented | 14281679 | is a Continuation of |
0 | 15384723 | 2015-09-23 | Patented | 14862609 | is a Continuation of |
Everything checks out! The oldest case that matches our critera is the one that our function picked!
Patent Term Extension#
Occasionally, the patent office moves slowly in granting patents. To prevent patentees from losing out on patent term, the USPTO can add back days lost due to USPTO delay. The fine-grained details aren’t important here, because under USPTO regs, the term extension is set when the patent issues, and cannot be changed. If the USPTO made a mistake in calculating the term extension, a petition has to be filed immediately. MPEP Sec. 2733.
For our purposes, the number we need is available under the pta_pte_summary composite object. Let’s take a look at an example:
[6]:
from pprint import pprint
pprint(next(p.pta_pte_summary.to_dict() for p in patents if p.pta_pte_summary.total_days > 0))
Row([('a_delay', 343),
('applicant_delay', 47),
('b_delay', 0),
('c_delay', 0),
('kind', 'PTA'),
('overlap_delay', 0),
('pto_adjustments', 0),
('pto_delay', 343),
('total_days', 296)])
All we’re interested in is “total days” which is a number of days to be added to the patent term. Let’s grab that data, and add it to our dataframe:
[7]:
pta_df = pd.DataFrame(
{
'PTA or PTE': [p.pta_pte_summary.total_days for p in patents]
}, index=[p.patent_number for p in patents])
expiry_df = default_expiry.join(pta_df, how="left")
expiry_df.head()
len(expiry_df.drop_duplicates())
[7]:
121
Lastly, let’s include that in our expiration date calculation
[8]:
expiry_df['extended_expiration_date'] = expiry_df.apply(lambda x: x['20_years_from_filing'] + relativedelta(days=x['PTA or PTE']), axis=1)
expiry_df.head()
[8]:
application_number | filing_date | number_of_parents | earliest_parent_case | earliest_parent_relationship | earliest_parent_filing_date | 20_years_from_filing | PTA or PTE | extended_expiration_date | |
---|---|---|---|---|---|---|---|---|---|
8428806 | 13675715 | 2012-11-13 | 4 | PCT/US08/77842 | is National Stage Entry of | 2008-09-26 | 2028-09-26 | 0 | 2028-09-26 |
9975508 | 14522352 | 2014-10-23 | 1 | 13872866 | is a Division of | 2013-04-29 | 2033-04-29 | 296 | 2034-02-19 |
9103143 | 13626864 | 2012-09-25 | 4 | 13626864 | Self | 2012-09-25 | 2032-09-25 | 413 | 2033-11-12 |
9761919 | 14189219 | 2014-02-25 | 0 | 14189219 | Self | 2014-02-25 | 2034-02-25 | 320 | 2035-01-11 |
9045030 | 14168351 | 2014-01-30 | 2 | 13308300 | is a Continuation of | 2011-11-30 | 2031-11-30 | 0 | 2031-11-30 |
Terminal Disclaimers#
The last step in calculating patent expiration dates is to see if the patent is subject to a terminal disclaimer. Unfortunately, the USApplication object can’t tell you what cases the application is terminally disclaimed over, but it can tell you whether a terminal disclaimer has been filed. That information is in the Transaction History. The “Terminal Disclaimer Filed” transaction code is “DIST” (See Transaction Codes)
Therefore, what we can do is add a flag to our expiration date calculation to tell us whether we need to go check the file history to determine if there is a terminal disclaimer. To make it useful, we’ill include the date the terminal disclaimer was filed (so we can find it easily) or if none, we’ll leave it blank:
[9]:
def terminal_disclaimer_present(app):
transactions = app.transactions
try:
disclaimer = next(t for t in transactions if t.code == 'DIST')
return disclaimer.date
except StopIteration:
return None
expiry_df['terminal_disclaimer_filed'] = [terminal_disclaimer_present(p) for p in patents]
expiry_df.sort_index().head(10)
[9]:
application_number | filing_date | number_of_parents | earliest_parent_case | earliest_parent_relationship | earliest_parent_filing_date | 20_years_from_filing | PTA or PTE | extended_expiration_date | terminal_disclaimer_filed | |
---|---|---|---|---|---|---|---|---|---|---|
10011157 | 14803747 | 2015-07-20 | 0 | 14803747 | Self | 2015-07-20 | 2035-07-20 | 379 | 2036-08-02 | None |
10018681 | 15002710 | 2016-01-21 | 1 | 15002710 | Self | 2016-01-21 | 2036-01-21 | 370 | 2037-01-25 | None |
10019066 | 13765363 | 2013-02-12 | 2 | 13764942 | is a Continuation of | 2013-02-12 | 2033-02-12 | 544 | 2034-08-10 | 2018-03-05 |
10023038 | 14703646 | 2015-05-04 | 3 | 13308300 | is a Continuation of | 2011-11-30 | 2031-11-30 | 0 | 2031-11-30 | None |
10046422 | 15099883 | 2016-04-15 | 1 | 13886672 | is a Division of | 2013-05-03 | 2033-05-03 | 320 | 2034-03-19 | None |
10131248 | 15384723 | 2016-12-20 | 6 | 12322218 | is a Continuation in part of | 2009-01-29 | 2029-01-29 | 0 | 2029-01-29 | 2018-07-27 |
10153116 | 14647777 | 2015-05-27 | 2 | PCT/US13/72596 | is National Stage Entry of | 2013-12-02 | 2033-12-02 | 0 | 2033-12-02 | None |
10166590 | 14865625 | 2015-09-25 | 0 | 14865625 | Self | 2015-09-25 | 2035-09-25 | 752 | 2037-10-16 | None |
10173739 | 15381456 | 2016-12-16 | 1 | 14839822 | is a Continuation of | 2015-08-28 | 2035-08-28 | 215 | 2036-03-30 | None |
10178805 | 14286670 | 2014-05-23 | 0 | 14286670 | Self | 2014-05-23 | 2034-05-23 | 339 | 2035-04-27 | None |
We can put this all together as:
[10]:
from dateutil.relativedelta import relativedelta
class Relationship():
pass
def earliest_term_parent(app):
parents = app.parent_continuity
term_parents = [p for p in parents
if p.relationship not in [
'Claims Priority from Provisional Application',
'is a Reissue of'
]]
if not term_parents:
# Create a dummy relationship if there are no matching relationships
earliest_parent = Relationship()
earliest_parent.parent_app_filing_date = app.app_filing_date
earliest_parent.relationship = 'Self'
earliest_parent.parent_appl_id = app.appl_id
return earliest_parent
earliest_parent = sorted(term_parents, key=lambda x: x.parent_app_filing_date)[0]
return earliest_parent
earliest_parents = [earliest_term_parent(p) for p in patents]
expiration_df = pd.DataFrame(data={
'application_number': [a.appl_id for a in patents],
'filing_date': [a.app_filing_date for a in patents],
'number_of_parents': [len(p.parent_continuity) for p in patents],
'earliest_parent_case': [p.parent_appl_id for p in earliest_parents],
'earliest_parent_relationship': [p.relationship for p in earliest_parents],
'earliest_parent_filing_date': [p.parent_app_filing_date for p in earliest_parents],
'20_years_from_filing': [p.parent_app_filing_date + relativedelta(years=20) for p in earliest_parents],
'pta_or_pte': [p.pta_pte_summary.total_days for p in patents],
}, index=[a.patent_number for a in patents])
expiration_df['20_years_from_earliest_parent'] = expiration_df['earliest_parent_filing_date'].apply(lambda x: x + relativedelta(years=20))
expiration_df['extended_expiration_date'] = expiration_df.apply(lambda x: x['20_years_from_earliest_parent'] + relativedelta(days=x['pta_or_pte']), axis=1)
expiration_df.head()
[10]:
application_number | filing_date | number_of_parents | earliest_parent_case | earliest_parent_relationship | earliest_parent_filing_date | 20_years_from_filing | pta_or_pte | 20_years_from_earliest_parent | extended_expiration_date | |
---|---|---|---|---|---|---|---|---|---|---|
8428806 | 13675715 | 2012-11-13 | 4 | PCT/US08/77842 | is National Stage Entry of | 2008-09-26 | 2028-09-26 | 0 | 2028-09-26 | 2028-09-26 |
9975508 | 14522352 | 2014-10-23 | 1 | 13872866 | is a Division of | 2013-04-29 | 2033-04-29 | 296 | 2033-04-29 | 2034-02-19 |
9103143 | 13626864 | 2012-09-25 | 4 | 13626864 | Self | 2012-09-25 | 2032-09-25 | 413 | 2032-09-25 | 2033-11-12 |
9761919 | 14189219 | 2014-02-25 | 0 | 14189219 | Self | 2014-02-25 | 2034-02-25 | 320 | 2034-02-25 | 2035-01-11 |
9045030 | 14168351 | 2014-01-30 | 2 | 13308300 | is a Continuation of | 2011-11-30 | 2031-11-30 | 0 | 2031-11-30 | 2031-11-30 |