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