Repository: pvlib/pvlib-python
Pull request #682 (2019-03-29 16:22:25)

move celltemp functions to celltemp.py, expose celltemp.pvsyst in ModelChain
 - [x] Closes  #678
 - [x] I am familiar with the [contributing guidelines](http://pvlib-python.readthedocs.io/en/latest/contributing.html).
 - [x] Fully tested. Added and/or modified tests to ensure correct behavior for all reasonable inputs. Tests (usually) must pass on the TravisCI and Appveyor testing services.
 - [x] Updates entries to `docs/sphinx/source/api.rst` for API changes.
 - [x] Adds description and name entries in the appropriate `docs/sphinx/source/whatsnew` file for all changes.
 - [x] Code quality and style is sufficient. Passes LGTM and SticklerCI checks.
 - [x] New code is fully documented. Includes sphinx/numpydoc compliant docstrings and comments in the code where necessary.
 - [x] Pull request is nearly complete and ready for detailed review.

Brief description of the problem and proposed solution (if not already fully described in the issue linked to above):

Move celltemp functions `sapm_celltemp` and `pvsyst_celltemp` to new `celltemp.py`.  #436 is relevant.
Changed argument names and argument order for `celltemp.sapm` for commonality with `celltemp.pvsyst`.
Changed output of `celltemp.pvsyst` to a Dataframe for commonality with `celltemp.sapm`
Add ``pvsyst`` as a value for `ModelChain.temp_model`
Corrected docstring for `pvsystem.PVSystem.sapm`.



cwhanse: @wholmgren how should we handle the methods that wrapped the deprecated `sapm_celltemp` and `pvsyst_celltemp` functions?

Also, I want to change the argument order for `celltemp.sapm` to the same order as `celltemp.pvsyst`, and possibly give `windspeed` a default value.  Is there a way to maintain the old argument order through the deprecation period?
wholmgren: > how should we handle the methods that wrapped the deprecated sapm_celltemp and pvsyst_celltemp functions?

If I understand the question correctly, the wrappers should be changed to call the new functions.

>  Is there a way to maintain the old argument order through the deprecation period?

I think you could do it like this:

```python
pvsyst_celltemp = deprecated('0.6.2', alternative='celltemp.pvsyst',
                             name='pvsyst_celltemp',
                             removal='0.7')(_function_that_calls_celltemp_pvsyst_with_right_arguments)

def _function_that_calls_celltemp_pvsyst_with_right_arguments...
```

I largely copied the deprecation function from matplotlib. I'm guessing we can find some examples of switching argument order in their PR history.

wholmgren: We should make an 0.6.2 release and then move on to the 0.7 series for this deprecation. The focus of 0.7 could be breaking up `pvsystem.py`.
cwhanse: > how should we handle the methods that wrapped the deprecated sapm_celltemp and pvsyst_celltemp functions?
> 
> If I understand the question correctly, the wrappers should be changed to call the new functions.

I would like to change the method name to parallel the new module.function pattern, to `PVSystem.celltemp_sapm` from `PVSystem.sapm_celltemp`. My question is how to deprecate the old method.


> Is there a way to maintain the old argument order through the deprecation period?
> 
> I think you could do it like this:
> pvsyst_celltemp = deprecated('0.6.2', alternative='celltemp.pvsyst',
>                              name='pvsyst_celltemp',
>                              removal='0.7')(_function_that_calls_celltemp_pvsyst_with_right_arguments)
> 

Aha, thanks.
cwhanse: > We should make an 0.6.2 release and then move on to the 0.7 series for this deprecation. The focus of 0.7 could be breaking up pvsystem.py.

Agree.  Am I implementing incorrectly?  Or are you saying that we should hold this PR for 0.7.
wholmgren: I'm saying we should hold this PR for 0.7. But we should also try to wrap up 0.6.2 soon so that this doesn't sit too long.
cwhanse: OK with me.  Let's get some milestone labels assigned.
adriesse: Comment on the function signatures: I would really prefer to see the model parameters as function parameters, i.e. `u0, u1`; `uc, uv`; `a, b`; etc.  An optional final parameter `presets=None` or something could be used to provide an alternate mechanism to specify coefficients.
cwhanse: This is ready for review
adriesse: My earlier comment seems to have gone unnoticed. I would really prefer to see the model parameters as function parameters, i.e. u0, u1; uc, uv; a, b; etc. An optional final parameter presets=None or something could be used to provide an alternate mechanism to specify coefficients.

Was there already a discussion about the pros and cons of a `model=` catch-all parameter? Did I miss something obvious again??
wholmgren: I agree with you @adriesse and I think now is the right time to make the change.
cwhanse: > My earlier comment seems to have gone unnoticed. I would really prefer to see the model parameters as function parameters, i.e. u0, u1; uc, uv; a, b; etc. An optional final parameter presets=None or something could be used to provide an alternate mechanism to specify coefficients.
> Was there already a discussion about the pros and cons of a model= catch-all parameter? Did I miss something obvious again??

Not unnoticed but certainly not addressed.

I like having the convenience of named parameter sets. I agree that having `model : str or dict-like` is an awkward way to provide both named and specific parameter inputs.  A proposal:
- at the function layer, discard the `model` kwarg as it is now, and replace with parameter kwargs specific to each model.
- keep the named parameter sets.
 - write a new function that accepts a model name and a value from the list of names, e.g., `freestanding` and returns the parameters, or throws an error if the model and name are inconsistent.  The primary use of this function is at the PVSystem and ModelChain layers.


adriesse: Glad this did not fall on deaf ears.

> at the function layer, discard the model kwarg as it is now, and replace with parameter kwargs specific to each model.

Good.

> keep the named parameter sets.

In some form or place, definitely.

>write a new function that accepts a model name and a value from the list of names, e.g., freestanding and returns the parameters, or throws an error if the model and name are inconsistent. The primary use of this function is at the PVSystem and ModelChain layers.

I think a single function for all models might become unwieldy.  In fact, it starts to look a lot like a model parameter database... could be a data file of some sort (yaml?) and a look-up function.

>One question: how do we record temperature model parameters for a PVSystem? 

I'm not a regular user of these Classes, but conceptually I would say that parameters related to module temperature should be appearing in both places. 
wholmgren: The function Cliff described sounds a lot like a basic dictionary lookup.
So, maybe we can skip the helper function if we provide documentation and
examples of how to use the dictionary of parameters. An Examples block
could be added to each of the function doc strings. Off the top of my head
I can’t think of a good place to specify the parameters at the object
layer.


On Fri, Jul 19, 2019 at 1:27 AM Anton Driesse <notifications@github.com>
wrote:

> Glad this did not fall on deaf ears.
>
> at the function layer, discard the model kwarg as it is now, and replace
> with parameter kwargs specific to each model.
>
> Good.
>
> keep the named parameter sets.
>
> In some form or place, definitely.
>
> write a new function that accepts a model name and a value from the list
> of names, e.g., freestanding and returns the parameters, or throws an error
> if the model and name are inconsistent. The primary use of this function is
> at the PVSystem and ModelChain layers.
>
> I think a single function for all models might become unwieldy. In fact,
> it starts to look a lot like a model parameter database... could be a data
> file of some sort (yaml?) and a look-up function.
>
> One question: how do we record temperature model parameters for a PVSystem?
>
> I'm not a regular user of these Classes, but conceptually I would say that
> parameters related to module temperature should be appearing in both places.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/pvlib/pvlib-python/pull/682?email_source=notifications&email_token=ABBOER2TNZD5MDBDFI55C73QAF3FXA5CNFSM4HCLBLKKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD2K6ZPI#issuecomment-513141949>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABBOERYC6JIHBFMF3ZK22A3QAF3FXANCNFSM4HCLBLKA>
> .
>

cwhanse: > The function Cliff described sounds a lot like a basic dictionary lookup.

It is.

> I think a single function for all models might become unwieldy. In fact, it starts to look a lot like a model parameter database... could be a data file of some sort (yaml?) and a look-up function.

Agree that it would be awkward, having thought about it a bit more.

> So, maybe we can skip the helper function if we provide documentation and
> examples of how to use the dictionary of parameters. An Examples block
> could be added to each of the function doc strings. 

Agree.

> Off the top of my head I can’t think of a good place to specify the parameters at the object
> layer.

Neither have I.

What about keeping `model` and adding the specific parameters?  In the `celltemp.<tempmodel>` function, we would use specific parameters, look up `model` if that is provided, or Error if neither is provided. The `PVSystem` method would default to one of the `model` strings.

adriesse: >What about keeping model and adding the specific parameters? 

This is basically what I meant with an optional parameter called "presets".  However, what I don't like about it is that the parameter values would be hidden inside the functions.  I think it would be preferable to be able to obtain the preset parameter values in order to inspect, adjust or whatever before using them.

I have thought about turning the functions into classes, which could each have their own dictionary of presets.
wholmgren: I was thinking of something like

```python
# celltemp.py
TEMP_MODEL_PARAMS = {
    'sapm': {'open_rack_cell_glassback': (-3.47, -.0594, 3),
             'roof_mount_cell_glassback': (-2.98, -.0471, 1),
             'open_rack_cell_polymerback': (-3.56, -.0750, 3),
             'insulated_back_polymerback': (-2.81, -.0455, 0),
             'open_rack_polymer_thinfilm_steel': (-3.58, -.113, 3),
             '22x_concentrator_tracker': (-3.23, -.130, 13)},
    'pvsyst': {'freestanding': (29.0, 0), 'insulated': (15.0, 0)}
}

# using args here, but could them make kwargs with defaults
def sapm(poa_global, temp_air, wind_speed, c1, c2, c3):

def pvsyst(poa_global, temp_air, wind_speed, c1, c2):

# user script (also included in Examples section of docstring)
params = TEMP_MODEL_PARAMS['sapm']['roof_mount_cell_glassback']
results = sapm(poa_global, temp_air, wind_speed, *params)
```

For the pure functions, I'd recommend against a `model` kwarg that performs the optional lookup. That sounds more appropriate at the `PVSystem` layer. But I don't feel strongly about that.
cwhanse: @wholmgren positional instead of named kwargs for c1, c2, etc.?
wholmgren: The main reason I used positional args in the example was because the dictionary of parameters contains tuples instead of another layer of dicts. I'd be ok with either args or kwargs with defaults.
adriesse: I vote for replacing the tuples with dicts and using **params.  You could get rid of the outer dict layer by having per-model presets.

cwhanse: Perhaps this compromise will meet all the needs @wholmgren @adriesse 

In `celltemp.py`:
```
def sapm(poa_global, temp_air, wind_speed, a, b, deltaT):
def pvsyst(poa_global, temp_air, wind_speed, constant_loss_factor, wind_loss_factor, eta_m=0.1, alpha_absorption=0.9):
```
In `pvsystem.py`:
```
def PVsystem(object):
    racking_model : None or string, default 'open_rack'
        Available options are 'open_rack', 'roof_mount', 'insulated_back'
    module_type : None or string, default 'glass-polymer'
        Available options are 'glass-polymer', 'glass-glass'
    temp_model_parameters : None or dict, default None
        For the SAPM cell temperature model, dict keys must be a, b, and deltaT
        For the Pvsyst cell temperature model, dict keys must be etc. etc.


def _get_temp_model_params(racking_model, module_type):
    ''' Returns temp_model_params as a dict
```

The private function is meant to help users of `ModelChain`.

One reason the current `TEMP_MODEL_PARAMS` is awkward is that the SAPM presets combine both the module description and the racking description, where the Pvsyst presets only use the racking description. Separating the racking description from the module_type may help people decide which presets to use and allows more general use of `racking_model` for other purposes.

cwhanse: Ready for review. Some points worth discussing:
- `PVSystem` has two new attributes, and one of them `temperature_model_parameters` is basically required to get a model to run. I'm OK with the requirement, not crazy about the attribute name.
- the current contract is to always return a DataFrame, because `celltemp.sapm` has two outputs (module and cell temperature). I think it is valuable for all `celltemp` functions to have common input and output order and type, to the extent practical. As @adriesse points out, the DataFrame output maybe annoying when all that is wanted is a single value.
- `celltemp.sapm` kwargs are the symbols from the reference document, `celltemp.pvsyst` kwargs are phrases that pvlib developers originated. I would prefer that all `celltemp` functions use a consistent style, and favor the symbols from the reference, although that is counter to python convention.
- let me know if the full docstring is worth the effort. I think it is, and would like to accumulate similar docstrings for other functions.
- I am considering dropping `TEMP_MODEL_PARAMS`, not sure I see it's utility with this refactoring.
- the testing in `ModelChain` uncovered that the default temperature model was `sapm` with parameters for a glass/glass module in open racking. An undocumented choice on our part.

Finally, I think a worthwhile goal for v0.8 is to overhaul management of model parameters. Related to not being please with `temperature_model_parameters`, topic for a new issue.
adriesse: > the current contract is to always return a DataFrame, because celltemp.sapm has two outputs (module and cell temperature). I think it is valuable for all celltemp functions to have common input and output order and type, to the extent practical. As @adriesse points out, the DataFrame output maybe annoying when all that is wanted is a single value. Maybe return Series or a tuple of Series instead of a DataFrame.

I think that most often a user will want only one type of temperature: cell or module; therefore choosing that in the function call and getting it back as a Series seems to be an easy way to avoid the DataFrame.

The module to cell temperature calculation (and vice versa) would make a good companion function.

> celltemp.sapm kwargs are the symbols from the reference document, celltemp.pvsyst kwargs are phrases that pvlib developers originated. I would prefer that all celltemp functions use a consistent style, and favor the symbols from the reference, although that is counter to python convention.

I think it may be hard to have a firm rule on this, but I usually like reusing source nomenclature.  

> let me know if the full docstring is worth the effort. I think it is, and would like to accumulate similar docstrings for other functions.

The `math` looks nice in `readthedocs` but does make doc strings less readable in the code.  I imagine it's not fun to compose these either, but as long as it does not become a deterrent (requirement) for new contributions/contributors I'm ok with it.

>I am considering dropping TEMP_MODEL_PARAMS, not sure I see its utility with this refactoring.

I can't imagine the full implications of this.  Does it mean not providing the "preset" values anywhere?

Would it be possible to make a small first PR with the new `celltemp` module and tests?  That way I can get moving #750.
wholmgren: 
> PVSystem has two new attributes, and one of them temperature_model_parameters is basically required to get a model to run. I'm OK with the requirement, not crazy about the attribute name.

No objection here. I don't understand the need for a separate `module_type` attribute though. That sounds like a `module_parameters` key.

> the current contract is to always return a DataFrame, because celltemp.sapm has two outputs (module and cell temperature). I think it is valuable for all celltemp functions to have common input and output order and type, to the extent practical. As @adriesse points out, the DataFrame output maybe annoying when all that is wanted is a single value. Maybe return Series or a tuple of Series instead of a DataFrame.

Some other pvlib functions that return DataFrames have a standard of: Series --> DataFrame; Scalar or ndarray --> OrderedDict of scalar or ndarray. Few if any pvlib functions return tuples of anything, though I am not opposed to this. I am not in favor of type promotion of a Series to a 1 column DataFrame. But in this case, I also like @adriesse's suggestion to implement separate functions for cell and module temperature.

> celltemp.sapm kwargs are the symbols from the reference document, celltemp.pvsyst kwargs are phrases that pvlib developers originated. I would prefer that all celltemp functions use a consistent style, and favor the symbols from the reference, although that is counter to python convention.

I don't have data to back this up, but I think a substantial number of non-PV experts use pvlib python and that they benefit from plain text parameter names. I am guessing that more than one user has wished that pvlib's PVWatts function had a `temperature_coefficient` argument instead of `gamma_pdc`. On the other hand, I think most people using the `ivtools` module will be familiar with the standard symbols. So I don't necessarily think we need a uniform style across the library. These cell temperature functions might fall somewhere in between. We should also keep in mind that the original references are not necessarily accessible to everyone (or even us). My recommendation remains that if a paper defines an equation by saying "where *x* is *not_too_long*, *y* is *getting_to_be_too_long_to_type*, and *alpha* is a fit parameter" then we should write a function `def doit(not_too_long, sensible_shorted_name, alpha)`.

While we're on this topic, I would like to raise the possibility of changing all of our `temp` abbreviations to `temperature`. I think `cell_temperature.py` is a lot nicer than `celltemp.py`, same for parameters and dataframe columns like `air_temp --> air_temperature`. I still think "temporary" half the time that I read this code.

> let me know if the full docstring is worth the effort. I think it is, and would like to accumulate similar docstrings for other functions.

I agree with @adriesse's comments on this topic. I'll add that the full docstring becomes much more important as the parameter names become less descriptive. 

> I am considering dropping TEMP_MODEL_PARAMS, not sure I see its utility with this refactoring.

I don't follow. It seems like it's still a good way for users to obtain the parameters when using the function API instead of the object API.

> Finally, I think a worthwhile goal for v0.8 is to overhaul management of model parameters. Related to not being please with temperature_model_parameters, topic for a new issue.

Agree. 
cwhanse: Thanks for the feedback.  Here's the plan:
1. replace `temp` with `temperature`
2. separate `sapm` into two functions, for cell and module temperature
3. replace `constant_loss_factor` and `wind_loss_factor` with `u_c` and `u_v` in `pvsyst_cell`. #651 is relevant. I agree that using a descriptive kwarg name is better when the reference provides one. In this case, we originated these kwarg names, so I think it better to stick with a name that is easily associated with the reference.
4. change output type to `float or Series`
5. keeping `TEMPERATURE_MODEL_PARAMETERS` but its only being used in the tests at present.
6. I'll rename `celltemp.py` when the PR is nearly complete.
7. I'll keep `module_type` as a separate attribute for now, because adding to `PVSystem.module_parameters` sort of suggests that a value should come from one of the parameter datasets. To be revisited with the parameter overhaul.
adriesse: > replace temp with temperature

It's just one opinion, but I am not a fan of many of those long names.  In the case of function parameters, the doc strings should be plenty to de-confuse anyone.  For module names I think there is even a python tradition of keeping them short and avoiding underscores--but I am neither up to date nor an expert on pythonics.  You might consider a module name like `thermal` or along those lines.

On a related note, how about allowing longer lines?

cwhanse: > replace temp with temperature

Going back on this in one instance: `temp_air` is a column in `weather` and is used as a variable throughout pvlib.  If we are changing this name, let's do it in a separate PR, it will break a number of interfaces.
wholmgren: > temp_air is a column in weather and is used as a variable throughout pvlib. If we are changing this name, let's do it in a separate PR, it will break a number of interfaces.

Ok with me but it would be nice if we can avoid a new instance of `temp` in the module name. I like @adriesse's `thermal.py` suggestion. 

>  For module names I think there is even a python tradition of keeping them short and avoiding underscores

I had to [look this up](https://www.python.org/dev/peps/pep-0008/#package-and-module-names). For module names there is a recommendation to keep it them short, but underscores are allowed for readability. For package names underscores are discouraged.

Otherwise, yes, let's address a `temp` to `temperature` refactor elsewhere.
cwhanse: > Ok with me but it would be nice if we can avoid a new instance of temp in the module name. I like @adriesse's thermal.py suggestion.

OK with `temperature.py`? To stay with nouns.

codecov shows that [this fixture](https://github.com/pvlib/pvlib-python/blob/9af1ca1751c1e5b94e5f2a136ec135814eed7b7d/pvlib/test/test_modelchain.py#L95) in `test_modelchain.py` is not used, and it appears that it has never been used.  Remove it?

The remaining lint errors are formatting to comply with line length limit, and I don't know how to make sticker happy.


wholmgren: > OK with temperature.py? To stay with nouns.

OK with me. 
adriesse: > OK with temperature.py? To stay with nouns.

"thermal" is also a noun... 
:)
adriesse: Scipy has both nouns and verbs; it even has a "special" module!
adriesse: I'm not promoting "thermal", just promoting fewer restrictions.
wholmgren: I have no objection to modules that are not nouns if that's what makes the most sense. I like both `temperature.py` and `thermal.py` while I think `temp.py` is horrible (I *hope* we can all agree on that). 
adriesse: Yes, despite its obvious merits, I too would vote against `temp.py`.  I actually also like `t.py`, but I might feel different about that in the morning.

cwhanse: Any objections to merging?  The flake8 [issue ](https://stackoverflow.com/questions/43746413/how-do-i-get-flake8-to-work-with-f811-errors) when importing a test fixture doesn't have a convenient fix.
wholmgren: @cwhanse are you able to build the documentation locally? There are a handful of errors. If you'd like, I can help debug with a pull request to your branch when I work out the examples I'm thinking of for the docstrings.
cwhanse: > @cwhanse are you able to build the documentation locally? There are a handful of errors. If you'd like, I can help debug with a pull request to your branch when I work out the examples I'm thinking of for the docstrings.

Yes. The celltemp [branch ](https://readthedocs.org/projects/cwhanse-pvlib-python/) is public, and I welcome proofreading or a PR.

Looks like I managed to cross the ivtools and celltemp branches again. I'll fix that last push shortly.
wholmgren: Working through the documentation errors made me realize that we've introduced a big breaking change: all users of `ModelChain` now must specify `PVSystem(temperature_model_parameters=...)`. I don't think we can do this without a deprecation period.
wholmgren: Did we discuss alternatives to `temperature_model_parameters`? It's awfully long for a keyword argument name. `temperature_parameters`? We don't include "model" in the other `*_parameters` arguments.
cwhanse: > Did we discuss alternatives to temperature_model_parameters? It's awfully long for a keyword argument name. temperature_parameters? We don't include "model" in the other *_parameters arguments.

We didn't, and I agree it's a long name.  The other *_parameters argument are associated with an object e.g. module. Here the object is the cell temperature model.  I'm open to ideas.  `temperature_parameters` seems vague to me.  We don't care for temp in place of temperature. parameters could be params but someone earlier objected. So `TEMP_MODEL_PARAMS` has become `TEMPERATURE_MODEL_PARAMETERS` and the `ModelChain` `PVSystem` attributes mimic that name.
cwhanse: > Working through the documentation errors made me realize that we've introduced a big breaking change: all users of ModelChain now must specify PVSystem(temperature_model_parameters=...). I don't think we can do this without a deprecation period.

Maintain the v0.6.3 behavior for deprecation?  The current behavior is to use `PVSystem.racking_model` default of `'open_rack_cell_glassback' and the SAPM temperature model. This is arguably a mistake (glas/glass default where most modules are glass/polymer) and is why I think we need to make `temperature_model_parameters` explicit and required.
wholmgren: opened a PR to your fork for mostly documentation changes.

I agree that it's good to require the temperature model parameters specification in `PVSystem`. My concern is that this will break a lot of user code without warning or explanation.
cwhanse: > opened a PR to your fork for mostly documentation changes.
> I agree that it's good to require the temperature model parameters specification in PVSystem. My concern is that this will break a lot of user code without warning or explanation.

Agree with the concern.  Warn and default to old behavior, unless you object. The warning will inform the user of the default behavior.
cwhanse: The existing code assign the glass/glass parameters but doesn’t inform the user. We can use the PVsystem attributes to maintain current behavior but users will get different results. I’m ok with that.

Sent from my iPhone

On Aug 26, 2019, at 11:00 AM, Will Holmgren <notifications@github.com<mailto:notifications@github.com>> wrote:


@wholmgren commented on this pull request.

________________________________

In pvlib/pvsystem.py<https://github.com/pvlib/pvlib-python/pull/682#discussion_r317697287>:

>
         Parameters
         ----------
-        See pvsystem.sapm_celltemp for details
+        poa_global : numeric or Series
+            Total incident irradiance in W/m^2.
+
+        temp_air : numeric or Series
+            Ambient dry bulb temperature in degrees C.
+
+        wind_speed : numeric or Series
+            Wind speed in m/s at a height of 10 meters.
+
+        parameter_set : string, default None


I think that if PVSystem is going to have a default module_type='glass_polymer' and a default racking_type='open_rack', then it should also use this information to attempt to assign default temperature_model_parameters instead of an empty dict. This would allow us to maintain compatibility with existing code.

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub<https://github.com/pvlib/pvlib-python/pull/682?email_source=notifications&email_token=ABJE2L3AIQP3PQGNNBDZCXLQGQD4PA5CNFSM4HCLBLKKYY3PNVWWK3TUL52HS4DFWFIHK3DMKJSXC5LFON2FEZLWNFSXPKTDN5WW2ZLOORPWSZGOCCWARIA#discussion_r317697287>, or mute the thread<https://github.com/notifications/unsubscribe-auth/ABJE2L6RPOMHLYAXT72CQ4TQGQD4PANCNFSM4HCLBLKA>.

cwhanse: @holmgren the principal changes since your last review:

- PVSystem.racking_model and PVSystem.module_type now are used to set temperature_model_parameters. 
- Because the temperature model is ModelChain.temperature_model and the model has to be consistent with PVSystem.temperature_model_parameters, which are set first, there's a check for consistency between the two, in ModelChain.temperature_model setter.  

The codecov flags a few `raise` lines that should be hit by the tests.
cwhanse: > A few mostly minor changes below and then let's move on. The big thing is the parameter_set kwargs -- I don't think those fit with the design philosophy of the PVSystem object.

Makes sense. That was an attempt to preserve convenience before I figured out how to use the `module_type` and `racking_model` attributes to do the same job. 
cwhanse: @adriesse any last comments? Hopefully this clears the way for #750
adriesse: I'm travelling at the moment, so can't do a detailed review.  I've been 
peripherally following the changes, but as long as the low-level 
functions are good I don't always pay attention to everything that 
happens with the classes.  I do look forward to doing #750 soon though.

On 2019-09-06 16:33, Cliff Hansen wrote:
>
> @adriesse <https://github.com/adriesse> any last comments? Hopefully 
> this clears the way for #750 
> <https://github.com/pvlib/pvlib-python/issues/750>
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub 
> <https://github.com/pvlib/pvlib-python/pull/682?email_source=notifications&email_token=ACEVQQ5FCQ4VYUANA2KPOY3QIJS47A5CNFSM4HCLBLKKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD6DA4IA#issuecomment-528879136>, 
> or mute the thread 
> <https://github.com/notifications/unsubscribe-auth/ACEVQQZPOXKNPKBHOBYZ7UTQIJS47ANCNFSM4HCLBLKA>.
>

-- 
Photovoltaic Performance Labs Germany
Emmy-Noether-Str. 2
79110 Freiburg
Germany

+49-761-8973-5603 (Office)
+49-174-532-7677 (Mobile)

www.pvperformancelabs.com


wholmgren: Thanks @adriesse. I think the lower-level functions are a good improvement over the current API. @cwhanse I suggest you go ahead and merge this monster.

Patch:
diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst
index 45ef9ec..8af2bf0 100644
--- a/docs/sphinx/source/api.rst
+++ b/docs/sphinx/source/api.rst
@@ -221,6 +221,16 @@ AOI modifiers
    pvsystem.ashraeiam
    pvsystem.sapm_aoi_loss
 
+PV temperature models
+---------------------
+
+.. autosummary::
+   :toctree: generated/
+
+   temperature.sapm_cell
+   temperature.sapm_module
+   temperature.pvsyst_cell
+
 Single diode models
 -------------------
 
@@ -258,29 +268,31 @@ Functions relevant for the SAPM model.
 
    pvsystem.sapm
    pvsystem.sapm_effective_irradiance
-   pvsystem.sapm_celltemp
    pvsystem.sapm_spectral_loss
    pvsystem.sapm_aoi_loss
    pvsystem.snlinverter
+   temperature.sapm_cell
 
-PVWatts model
+Pvsyst model
 -------------
 
+Functions relevant for the Pvsyst model.
+
 .. autosummary::
    :toctree: generated/
 
-   pvsystem.pvwatts_dc
-   pvsystem.pvwatts_ac
-   pvsystem.pvwatts_losses
-   pvsystem.pvwatts_losses
+   temperature.pvsyst_cell
 
-PVsyst model
-------------
+PVWatts model
+-------------
 
 .. autosummary::
    :toctree: generated/
 
-   pvsystem.pvsyst_celltemp
+   pvsystem.pvwatts_dc
+   pvsystem.pvwatts_ac
+   pvsystem.pvwatts_losses
+   pvsystem.pvwatts_losses
 
 Other
 -----
@@ -472,7 +484,7 @@ ModelChain properties that are aliases for your specific modeling functions.
    modelchain.ModelChain.ac_model
    modelchain.ModelChain.aoi_model
    modelchain.ModelChain.spectral_model
-   modelchain.ModelChain.temp_model
+   modelchain.ModelChain.temperature_model
    modelchain.ModelChain.losses_model
    modelchain.ModelChain.effective_irradiance_model
 
@@ -500,6 +512,7 @@ ModelChain model definitions.
    modelchain.ModelChain.sapm_spectral_loss
    modelchain.ModelChain.no_spectral_loss
    modelchain.ModelChain.sapm_temp
+   modelchain.ModelChain.pvsyst_temp
    modelchain.ModelChain.pvwatts_losses
    modelchain.ModelChain.no_extra_losses
 
@@ -516,7 +529,7 @@ on the information in the associated :py:class:`~pvsystem.PVSystem` object.
    modelchain.ModelChain.infer_ac_model
    modelchain.ModelChain.infer_aoi_model
    modelchain.ModelChain.infer_spectral_model
-   modelchain.ModelChain.infer_temp_model
+   modelchain.ModelChain.infer_temperature_model
    modelchain.ModelChain.infer_losses_model
 
 Functions
diff --git a/docs/sphinx/source/clearsky.rst b/docs/sphinx/source/clearsky.rst
index c47212f..669ca07 100644
--- a/docs/sphinx/source/clearsky.rst
+++ b/docs/sphinx/source/clearsky.rst
@@ -68,7 +68,7 @@ returns a :py:class:`pandas.DataFrame`.
 
     In [1]: tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson')
 
-    In [1]: times = pd.DatetimeIndex(start='2016-07-01', end='2016-07-04', freq='1min', tz=tus.tz)
+    In [1]: times = pd.date_range(start='2016-07-01', end='2016-07-04', freq='1min', tz=tus.tz)
 
     In [1]: cs = tus.get_clearsky(times)  # ineichen with climatology table by default
 
@@ -168,7 +168,7 @@ varies from 300 m to 1500 m.
 
 .. ipython::
 
-    In [1]: times = pd.DatetimeIndex(start='2015-01-01', end='2016-01-01', freq='1D')
+    In [1]: times = pd.date_range(start='2015-01-01', end='2016-01-01', freq='1D')
 
     In [1]: sites = [(32, -111, 'Tucson1'), (32.2, -110.9, 'Tucson2'),
        ...:          (33.5, -112.1, 'Phoenix'), (35.1, -106.6, 'Albuquerque')]
@@ -608,7 +608,7 @@ GHI data. We first generate and plot the clear sky and measured data.
 
     abq = Location(35.04, -106.62, altitude=1619)
 
-    times = pd.DatetimeIndex(start='2012-04-01 10:30:00', tz='Etc/GMT+7', periods=30, freq='1min')
+    times = pd.date_range(start='2012-04-01 10:30:00', tz='Etc/GMT+7', periods=30, freq='1min')
 
     cs = abq.get_clearsky(times)
 
diff --git a/docs/sphinx/source/forecasts.rst b/docs/sphinx/source/forecasts.rst
index f959982..56e88ac 100644
--- a/docs/sphinx/source/forecasts.rst
+++ b/docs/sphinx/source/forecasts.rst
@@ -440,6 +440,7 @@ for details.
 .. ipython:: python
 
     from pvlib.pvsystem import PVSystem, retrieve_sam
+    from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
     from pvlib.tracking import SingleAxisTracker
     from pvlib.modelchain import ModelChain
 
@@ -447,12 +448,10 @@ for details.
     cec_inverters = retrieve_sam('cecinverter')
     module = sandia_modules['Canadian_Solar_CS5P_220M___2009_']
     inverter = cec_inverters['SMA_America__SC630CP_US_315V__CEC_2012_']
+    temperature_model_parameters = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']
 
     # model a big tracker for more fun
-    system = SingleAxisTracker(module_parameters=module,
-                               inverter_parameters=inverter,
-                               modules_per_string=15,
-                               strings_per_inverter=300)
+    system = SingleAxisTracker(module_parameters=module, inverter_parameters=inverter, temperature_model_parameters=temperature_model_parameters, modules_per_string=15, strings_per_inverter=300)
 
     # fx is a common abbreviation for forecast
     fx_model = GFS()
@@ -480,9 +479,9 @@ Here's the forecast plane of array irradiance...
 
 .. ipython:: python
 
-    mc.temps.plot();
+    mc.cell_temperature.plot();
     @savefig pv_temps.png width=6in
-    plt.ylabel('Temperature (C)');
+    plt.ylabel('Cell Temperature (C)');
     @suppress
     plt.close();
 
diff --git a/docs/sphinx/source/introexamples.rst b/docs/sphinx/source/introexamples.rst
index 6cd0d54..e1b01d9 100644
--- a/docs/sphinx/source/introexamples.rst
+++ b/docs/sphinx/source/introexamples.rst
@@ -30,7 +30,7 @@ configuration at a handful of sites listed below.
     import pandas as pd
     import matplotlib.pyplot as plt
 
-    naive_times = pd.DatetimeIndex(start='2015', end='2016', freq='1h')
+    naive_times = pd.date_range(start='2015', end='2016', freq='1h')
 
     # very approximate
     # latitude, longitude, name, altitude, timezone
@@ -46,6 +46,7 @@ configuration at a handful of sites listed below.
     sapm_inverters = pvlib.pvsystem.retrieve_sam('cecinverter')
     module = sandia_modules['Canadian_Solar_CS5P_220M___2009_']
     inverter = sapm_inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_']
+    temperature_model_parameters = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']
 
     # specify constant ambient air temp and wind for simplicity
     temp_air = 20
@@ -88,12 +89,13 @@ to accomplish our system modeling goal:
                                                             cs['dni'], cs['ghi'], cs['dhi'],
                                                             dni_extra=dni_extra,
                                                             model='haydavies')
-        temps = pvlib.pvsystem.sapm_celltemp(total_irrad['poa_global'],
-                                             wind_speed, temp_air)
+        tcell = pvlib.temperature.sapm_cell(total_irrad['poa_global'],
+                                            temp_air, wind_speed,
+                                            **temperature_model_parameters)
         effective_irradiance = pvlib.pvsystem.sapm_effective_irradiance(
             total_irrad['poa_direct'], total_irrad['poa_diffuse'],
             am_abs, aoi, module)
-        dc = pvlib.pvsystem.sapm(effective_irradiance, temps['temp_cell'], module)
+        dc = pvlib.pvsystem.sapm(effective_irradiance, tcell, module)
         ac = pvlib.pvsystem.snlinverter(dc['v_mp'], dc['p_mp'], inverter)
         annual_energy = ac.sum()
         energies[name] = annual_energy
@@ -149,7 +151,8 @@ by examining the parameters defined for the module.
     from pvlib.modelchain import ModelChain
 
     system = PVSystem(module_parameters=module,
-                      inverter_parameters=inverter)
+                      inverter_parameters=inverter,
+                      temperature_model_parameters=temperature_model_parameters)
 
     energies = {}
     for latitude, longitude, name, altitude, timezone in coordinates:
@@ -214,6 +217,7 @@ modeling goal:
     for latitude, longitude, name, altitude, timezone in coordinates:
         localized_system = LocalizedPVSystem(module_parameters=module,
                                              inverter_parameters=inverter,
+                                             temperature_model_parameters=temperature_model_parameters,
                                              surface_tilt=latitude,
                                              surface_azimuth=180,
                                              latitude=latitude,
@@ -229,15 +233,15 @@ modeling goal:
                                                       clearsky['dni'],
                                                       clearsky['ghi'],
                                                       clearsky['dhi'])
-        temps = localized_system.sapm_celltemp(total_irrad['poa_global'],
-                                               wind_speed, temp_air)
+        tcell = localized_system.sapm_celltemp(total_irrad['poa_global'],
+                                               temp_air, wind_speed)
         aoi = localized_system.get_aoi(solar_position['apparent_zenith'],
                                        solar_position['azimuth'])
         airmass = localized_system.get_airmass(solar_position=solar_position)
         effective_irradiance = localized_system.sapm_effective_irradiance(
             total_irrad['poa_direct'], total_irrad['poa_diffuse'],
             airmass['airmass_absolute'], aoi)
-        dc = localized_system.sapm(effective_irradiance, temps['temp_cell'])
+        dc = localized_system.sapm(effective_irradiance, tcell)
         ac = localized_system.snlinverter(dc['v_mp'], dc['p_mp'])
         annual_energy = ac.sum()
         energies[name] = annual_energy
diff --git a/docs/sphinx/source/modelchain.rst b/docs/sphinx/source/modelchain.rst
index 1b32480..4025451 100644
--- a/docs/sphinx/source/modelchain.rst
+++ b/docs/sphinx/source/modelchain.rst
@@ -45,6 +45,8 @@ objects, module data, and inverter data.
     from pvlib.pvsystem import PVSystem
     from pvlib.location import Location
     from pvlib.modelchain import ModelChain
+    from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
+    temperature_model_parameters = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']
 
     # load some module and inverter specifications
     sandia_modules = pvlib.pvsystem.retrieve_sam('SandiaMod')
@@ -61,7 +63,8 @@ object.
     location = Location(latitude=32.2, longitude=-110.9)
     system = PVSystem(surface_tilt=20, surface_azimuth=200,
                       module_parameters=sandia_module,
-                      inverter_parameters=cec_inverter)
+                      inverter_parameters=cec_inverter,
+                      temperature_model_parameters=temperature_model_parameters)
     mc = ModelChain(system, location)
 
 Printing a ModelChain object will display its models.
@@ -87,6 +90,10 @@ examples are shown below.
 
     mc.aoi
 
+.. ipython:: python
+
+    mc.cell_temperature
+
 .. ipython:: python
 
     mc.dc
@@ -141,8 +148,11 @@ model, AC model, AOI loss model, and spectral loss model.
 
 .. ipython:: python
 
-    sapm_system = PVSystem(module_parameters=sandia_module, inverter_parameters=cec_inverter)
-    mc = ModelChain(system, location)
+    sapm_system = PVSystem(
+        module_parameters=sandia_module,
+        inverter_parameters=cec_inverter,
+        temperature_model_parameters=temperature_model_parameters)
+    mc = ModelChain(sapm_system, location)
     print(mc)
 
 .. ipython:: python
@@ -160,7 +170,10 @@ information to determine which of those models to choose.
 
 .. ipython:: python
 
-    pvwatts_system = PVSystem(module_parameters={'pdc0': 240, 'gamma_pdc': -0.004})
+    pvwatts_system = PVSystem(
+        module_parameters={'pdc0': 240, 'gamma_pdc': -0.004},
+        inverter_parameters={'pdc0': 240},
+        temperature_model_parameters=temperature_model_parameters)
     mc = ModelChain(pvwatts_system, location,
                     aoi_model='physical', spectral_model='no_loss')
     print(mc)
@@ -176,8 +189,11 @@ functions for a PVSystem that contains SAPM-specific parameters.
 
 .. ipython:: python
 
-    sapm_system = PVSystem(module_parameters=sandia_module, inverter_parameters=cec_inverter)
-    mc = ModelChain(system, location, aoi_model='physical', spectral_model='no_loss')
+    sapm_system = PVSystem(
+        module_parameters=sandia_module,
+        inverter_parameters=cec_inverter,
+        temperature_model_parameters=temperature_model_parameters)
+    mc = ModelChain(sapm_system, location, aoi_model='physical', spectral_model='no_loss')
     print(mc)
 
 .. ipython:: python
@@ -264,21 +280,24 @@ the ModelChain.pvwatts_dc method is shown below. Its only argument is
 
 The ModelChain.pvwatts_dc method calls the pvwatts_dc method of the
 PVSystem object that we supplied using data that is stored in its own
-``effective_irradiance`` and ``temps`` attributes. Then it assigns the
+``effective_irradiance`` and ``cell_temperature`` attributes. Then it assigns the
 result to the ``dc`` attribute of the ModelChain object. The code below
 shows a simple example of this.
 
 .. ipython:: python
 
     # make the objects
-    pvwatts_system = PVSystem(module_parameters={'pdc0': 240, 'gamma_pdc': -0.004})
+    pvwatts_system = PVSystem(
+        module_parameters={'pdc0': 240, 'gamma_pdc': -0.004},
+        inverter_parameters={'pdc0': 240},
+        temperature_model_parameters=temperature_model_parameters)
     mc = ModelChain(pvwatts_system, location,
                     aoi_model='no_loss', spectral_model='no_loss')
 
     # manually assign data to the attributes that ModelChain.pvwatts_dc will need.
     # for standard workflows, run_model would assign these attributes.
     mc.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')])
-    mc.temps = pd.DataFrame({'temp_cell': 50, 'temp_module': 50}, index=[pd.Timestamp('20170401 1200-0700')])
+    mc.cell_temperature = pd.Series(50, index=[pd.Timestamp('20170401 1200-0700')])
 
     # run ModelChain.pvwatts_dc and look at the result
     mc.pvwatts_dc();
@@ -304,13 +323,16 @@ PVSystem.scale_voltage_current_power method.
 .. ipython:: python
 
     # make the objects
-    sapm_system = PVSystem(module_parameters=sandia_module, inverter_parameters=cec_inverter)
+    sapm_system = PVSystem(
+        module_parameters=sandia_module,
+        inverter_parameters=cec_inverter,
+        temperature_model_parameters=temperature_model_parameters)
     mc = ModelChain(sapm_system, location)
 
     # manually assign data to the attributes that ModelChain.sapm will need.
     # for standard workflows, run_model would assign these attributes.
     mc.effective_irradiance = pd.Series(1000, index=[pd.Timestamp('20170401 1200-0700')])
-    mc.temps = pd.DataFrame({'temp_cell': 50, 'temp_module': 50}, index=[pd.Timestamp('20170401 1200-0700')])
+    mc.cell_temperature = pd.Series(50, index=[pd.Timestamp('20170401 1200-0700')])
 
     # run ModelChain.sapm and look at the result
     mc.sapm();
@@ -333,7 +355,10 @@ section.
 
 .. ipython:: python
 
-    pvwatts_system = PVSystem(module_parameters={'pdc0': 240, 'gamma_pdc': -0.004})
+    pvwatts_system = PVSystem(
+        module_parameters={'pdc0': 240, 'gamma_pdc': -0.004},
+        inverter_parameters={'pdc0': 240},
+        temperature_model_parameters=temperature_model_parameters)
     mc = ModelChain(pvwatts_system, location,
                     aoi_model='no_loss', spectral_model='no_loss')
     mc.dc_model.__func__
@@ -403,18 +428,26 @@ function if you wanted to.
         return mc
 
 
-    def pvusa_ac_mc_wrapper(mc):
+    def pvusa_ac_mc(mc):
         # keep it simple
         mc.ac = mc.dc
         return mc
 
+
+    def no_loss_temperature(mc):
+        # keep it simple
+        mc.cell_temperature = mc.weather['temp_air']
+        return mc
+
+
 .. ipython:: python
 
     module_parameters = {'a': 0.2, 'b': 0.00001, 'c': 0.001, 'd': -0.00005}
     pvusa_system = PVSystem(module_parameters=module_parameters)
 
     mc = ModelChain(pvusa_system, location,
-                    dc_model=pvusa_mc_wrapper, ac_model=pvusa_ac_mc_wrapper,
+                    dc_model=pvusa_mc_wrapper, ac_model=pvusa_ac_mc,
+                    temperature_model=no_loss_temperature,
                     aoi_model='no_loss', spectral_model='no_loss')
 
 A ModelChain object uses Python’s functools.partial function to assign
diff --git a/docs/sphinx/source/whatsnew/v0.7.0.rst b/docs/sphinx/source/whatsnew/v0.7.0.rst
index 20668a3..9dfd58e 100644
--- a/docs/sphinx/source/whatsnew/v0.7.0.rst
+++ b/docs/sphinx/source/whatsnew/v0.7.0.rst
@@ -1,7 +1,7 @@
 .. _whatsnew_0700:
 
 v0.7.0 (MONTH DAY, YEAR)
----------------------
+------------------------
 
 This is a major release that drops support for Python 2 and Python 3.4. We
 recommend all users of v0.6.3 upgrade to this release after checking API
@@ -12,6 +12,63 @@ compatibility notes.
 
 API Changes
 ~~~~~~~~~~~
+* Changes related to cell temperature models (:issue:`678`):
+   * Changes to functions
+      - Moved functions for cell temperature from `pvsystem.py` to `temperature.py`.
+      - Renamed `pvsystem.sapm_celltemp` and `pvsystem.pvsyst_celltemp`
+        to `temperature.sapm_cell` and `temperature.pvsyst_cell`.
+      - `temperature.sapm_cell` returns only the cell temperature, whereas the
+         old `pvsystem.sapm_celltemp` returned a `DataFrame` with both cell and module temperatures.
+      - Created `temperature.sapm_module` to return module temperature using the SAPM temperature model.
+      - Changed the order of arguments for`pvsystem.sapm_celltemp`,
+        `pvsystem.pvsyst_celltemp` and `PVSystem.sapm_celltemp` to be consistent 
+        among cell temperature model functions.
+      - Removed `model` as a kwarg from `temperature.sapm_cell` and
+        `temperature.pvsyst_cell`. These functions now require model-specific parameters.
+      - Added the argument `irrad_ref`, default value 1000, to `temperature.sapm_cell`.
+   * Changes to named temperature model parameter sets
+      - Renamed `pvsystem.TEMP_MODEL_PARAMS` to `temperature.TEMPERATURE_MODEL_PARAMETERS`.
+      - `temperature.TEMPERATURE_MODEL_PARAMETERS` uses dict rather than
+        tuple for a parameter set.
+      - Names for parameter sets in `temperature.TEMPERATURE_MODEL_PARAMETERS` have changed.
+      - Parameter sets for the SAPM cell temperature model named
+        'open_rack_polymer_thinfilm_steel' and '22x_concentrator_tracker'
+        are considered obsolete and have been removed.
+   * Changes to `PVSystem` class
+      - Changed the `model` kwarg in `PVSystem.sapm_celltemp` and 
+        `PVSystem.pvsyst_celltemp` to `parameter_set`. `parameter_set` expects
+        a str which is a valid key for `temperature.TEMPERATURE_MODEL_PARAMETERS`
+        for the corresponding temperature model.
+      - Added an attribute `PVSystem.module_type` (str) to record module
+        front and back materials, default is `glass_polymer`.
+      - Changed meaning of `PVSystem.racking_model` to describe racking
+        only, e.g., default is `open_rack`.
+      - Added an attribute `PVSystem.temperature_model_parameters` (dict).
+        to contain temperature model parameters.
+      - If `PVSystem.temperature_model_parameters` is not specified and
+        `PVSystem.racking_model` and `PVSystem.module_type` combine to a valid
+        parameter set name for the SAPM cell temperature model, that parameter
+        set is assigned to `PVSystem.temperature_model_parameters`. Otherwise
+        `PVSystem.temperature_model_parameters` is assigned an empty dict. The
+        result is that the default parameter set for SAPM cell temperature model
+        is `open_rack_glass_polymer`; the old default was `open_rack_glass_glass`.
+   * Changes to `ModelChain` class
+      - `ModelChain.temp_model` renamed to `ModelChain.temperature_model`.
+      - `ModelChain.temperature_model` now defaults to `None`. The temperature
+        model can be inferred from `PVSystem.temperature_model_parameters`.
+      - `ModelChain.temperature_model_parameters` now defaults to `None`. The temperature
+        model can be inferred from `PVSystem.temperature_model_parameters`.
+      - `ModelChain.temps` attribute renamed to `ModelChain.cell_temperature`,
+         and its datatype is now `numeric` rather than `DataFrame`.
+      - If `PVSystem.temperature_model_parameters` is not specified, `ModelChain`
+        defaults to old behavior, using the SAPM temperature model with parameter
+        set `open_rack_glass_glass`. This behavior is deprecated, and will be
+         removed in v0.8. In v0.8 `PVSystem.temperature_model_parameters` will
+          be required for `ModelChain`.
+      - Implemented `pvsyst` as an option for `ModelChain.temperature_model`.
+      - `modelchain.basic_chain` has a new required argument
+        `temperature_model_parameters`.
+
 * Calling :py:func:`pvlib.pvsystem.retrieve_sam` with no parameters will raise
   an exception instead of displaying a dialog.
 
@@ -34,8 +91,14 @@ Bug fixes
 
 Testing
 ~~~~~~~
-* Added 30 minutes to timestamps in `test_psm3.csv` to match change in NSRDB (:issue:`733`)
+* Added 30 minutes to timestamps in `test_psm3.csv` to match change
+  in NSRDB (:issue:`733`)
 * Added tests for methods in bifacial.py.
+* Added tests for changes to cell temperature models.
+
+Documentation
+~~~~~~~~~~~~~
+* Corrected docstring for `pvsystem.PVSystem.sapm`
 
 Removal of prior version deprecations
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -55,4 +118,5 @@ Contributors
 * Oscar Dowson (:ghuser:`odow`)
 * Anton Driesse (:ghuser:`adriesse`)
 * Alexander Morgan (:ghuser:`alexandermorgan`)
+* Cliff Hansen (:ghuser:`cwhanse`)
 * Miguel Sánchez de León Peque (:ghuser:`Peque`)
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
index 1455a57..3244dc2 100644
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -10,7 +10,8 @@ from functools import partial
 import warnings
 import pandas as pd
 
-from pvlib import (solarposition, pvsystem, clearsky, atmosphere, tools)
+from pvlib import (atmosphere, clearsky, pvsystem, solarposition, temperature,
+                   tools)
 from pvlib.tracking import SingleAxisTracker
 import pvlib.irradiance  # avoid name conflict with full import
 from pvlib.pvsystem import DC_MODEL_PARAMS
@@ -18,7 +19,8 @@ from pvlib._deprecation import pvlibDeprecationWarning
 
 
 def basic_chain(times, latitude, longitude,
-                module_parameters, inverter_parameters,
+                module_parameters, temperature_model_parameters,
+                inverter_parameters,
                 irradiance=None, weather=None,
                 surface_tilt=None, surface_azimuth=None,
                 orientation_strategy=None,
@@ -46,10 +48,16 @@ def basic_chain(times, latitude, longitude,
         Use decimal degrees notation.
 
     module_parameters : None, dict or Series
-        Module parameters as defined by the SAPM.
+        Module parameters as defined by the SAPM. See pvsystem.sapm for
+        details.
+
+    temperature_model_parameters : None, dict or Series.
+        Temperature model parameters as defined by the SAPM.
+        See temperature.sapm_cell for details.
 
     inverter_parameters : None, dict or Series
-        Inverter parameters as defined by the CEC.
+        Inverter parameters as defined by the CEC. See pvsystem.snlinverter for
+        details.
 
     irradiance : None or DataFrame, default None
         If None, calculates clear sky data.
@@ -163,15 +171,16 @@ def basic_chain(times, latitude, longitude,
     if weather is None:
         weather = {'wind_speed': 0, 'temp_air': 20}
 
-    temps = pvsystem.sapm_celltemp(total_irrad['poa_global'],
-                                   weather['wind_speed'],
-                                   weather['temp_air'])
+    cell_temperature = temperature.sapm_cell(
+        total_irrad['poa_global'], weather['temp_air'], weather['wind_speed'],
+        temperature_model_parameters['a'], temperature_model_parameters['b'],
+        temperature_model_parameters['deltaT'])
 
     effective_irradiance = pvsystem.sapm_effective_irradiance(
         total_irrad['poa_direct'], total_irrad['poa_diffuse'], airmass, aoi,
         module_parameters)
 
-    dc = pvsystem.sapm(effective_irradiance, temps['temp_cell'],
+    dc = pvsystem.sapm(effective_irradiance, cell_temperature,
                        module_parameters)
 
     ac = pvsystem.snlinverter(dc['v_mp'], dc['p_mp'], inverter_parameters)
@@ -272,9 +281,9 @@ class ModelChain(object):
         'first_solar', 'no_loss'. The ModelChain instance will be passed
         as the first argument to a user-defined function.
 
-    temp_model: str or function, default 'sapm'
-        Valid strings are 'sapm'. The ModelChain instance will be passed
-        as the first argument to a user-defined function.
+    temperature_model: None, str or function, default None
+        Valid strings are 'sapm' and 'pvsyst'. The ModelChain instance will be
+        passed as the first argument to a user-defined function.
 
     losses_model: str or function, default 'no_loss'
         Valid strings are 'pvwatts', 'no_loss'. The ModelChain instance
@@ -295,11 +304,12 @@ class ModelChain(object):
                  solar_position_method='nrel_numpy',
                  airmass_model='kastenyoung1989',
                  dc_model=None, ac_model=None, aoi_model=None,
-                 spectral_model=None, temp_model='sapm',
+                 spectral_model=None, temperature_model=None,
                  losses_model='no_loss', name=None, **kwargs):
 
         self.name = name
         self.system = system
+
         self.location = location
         self.clearsky_model = clearsky_model
         self.transposition_model = transposition_model
@@ -311,7 +321,24 @@ class ModelChain(object):
         self.ac_model = ac_model
         self.aoi_model = aoi_model
         self.spectral_model = spectral_model
-        self.temp_model = temp_model
+
+        # TODO: deprecated kwarg temp_model. Remove use of temp_model in v0.8
+        temp_model = kwargs.pop('temp_model', None)
+        if temp_model is not None:
+            warnings.warn('The temp_model keyword argument is deprecated. Use '
+                          'temperature_model instead', pvlibDeprecationWarning)
+            if temperature_model is None:
+                temperature_model = temp_model
+            elif temp_model == temperature_model:
+                warnings.warn('Provide only one of temperature_model or '
+                              'temp_model (deprecated).')
+            else:
+                raise ValueError(
+                    'Conflicting temperature_model {} and temp_model {}. '
+                    'temp_model is deprecated. Specify only temperature_model.'
+                    .format(temperature_model, temp_model))
+        self.temperature_model = temperature_model
+
         self.losses_model = losses_model
         self.orientation_strategy = orientation_strategy
 
@@ -324,7 +351,7 @@ class ModelChain(object):
             'name', 'orientation_strategy', 'clearsky_model',
             'transposition_model', 'solar_position_method',
             'airmass_model', 'dc_model', 'ac_model', 'aoi_model',
-            'spectral_model', 'temp_model', 'losses_model'
+            'spectral_model', 'temperature_model', 'losses_model'
             ]
 
         def getmcattr(self, attr):
@@ -420,7 +447,7 @@ class ModelChain(object):
 
     def sapm(self):
         self.dc = self.system.sapm(self.effective_irradiance/1000.,
-                                   self.temps['temp_cell'])
+                                   self.cell_temperature)
 
         self.dc = self.system.scale_voltage_current_power(self.dc)
 
@@ -430,7 +457,7 @@ class ModelChain(object):
         (photocurrent, saturation_current, resistance_series,
          resistance_shunt, nNsVth) = (
             self.system.calcparams_desoto(self.effective_irradiance,
-                                          self.temps['temp_cell']))
+                                          self.cell_temperature))
 
         self.diode_params = (photocurrent, saturation_current,
                              resistance_series,
@@ -448,7 +475,7 @@ class ModelChain(object):
         (photocurrent, saturation_current, resistance_series,
          resistance_shunt, nNsVth) = (
             self.system.calcparams_cec(self.effective_irradiance,
-                                       self.temps['temp_cell']))
+                                       self.cell_temperature))
 
         self.diode_params = (photocurrent, saturation_current,
                              resistance_series,
@@ -466,7 +493,7 @@ class ModelChain(object):
         (photocurrent, saturation_current, resistance_series,
          resistance_shunt, nNsVth) = (
             self.system.calcparams_pvsyst(self.effective_irradiance,
-                                          self.temps['temp_cell']))
+                                          self.cell_temperature))
 
         self.diode_params = (photocurrent, saturation_current,
                              resistance_series,
@@ -485,7 +512,7 @@ class ModelChain(object):
         (photocurrent, saturation_current, resistance_series,
          resistance_shunt, nNsVth) = (
             self.system.calcparams_desoto(self.effective_irradiance,
-                                          self.temps['temp_cell']))
+                                          self.cell_temperature))
 
         self.desoto = (photocurrent, saturation_current, resistance_series,
                        resistance_shunt, nNsVth)
@@ -500,7 +527,7 @@ class ModelChain(object):
 
     def pvwatts_dc(self):
         self.dc = self.system.pvwatts_dc(self.effective_irradiance,
-                                         self.temps['temp_cell'])
+                                         self.cell_temperature)
         return self
 
     @property
@@ -659,29 +686,53 @@ class ModelChain(object):
         return self
 
     @property
-    def temp_model(self):
-        return self._temp_model
+    def temperature_model(self):
+        return self._temperature_model
 
-    @temp_model.setter
-    def temp_model(self, model):
+    @temperature_model.setter
+    def temperature_model(self, model):
         if model is None:
-            self._temp_model = self.infer_temp_model()
+            self._temperature_model = self.infer_temperature_model()
         elif isinstance(model, str):
             model = model.lower()
             if model == 'sapm':
-                self._temp_model = self.sapm_temp
+                self._temperature_model = self.sapm_temp
+            elif model == 'pvsyst':
+                self._temperature_model = self.pvsyst_temp
             else:
-                raise ValueError(model + ' is not a valid temp model')
+                raise ValueError(model + ' is not a valid temperature model')
+            # check system.temperature_model_parameters for consistency
+            name_from_params = self.infer_temperature_model().__name__
+            if self._temperature_model.__name__ != name_from_params:
+                raise ValueError(
+                    'Temperature model {} is inconsistent with '
+                    'PVsystem.temperature_model_parameters {}'.format(
+                        self._temperature_model.__name__,
+                        self.system.temperature_model_parameters))
         else:
-            self._temp_model = partial(model, self)
-
-    def infer_temp_model(self):
-        raise NotImplementedError
+            self._temperature_model = partial(model, self)
+
+    def infer_temperature_model(self):
+        params = set(self.system.temperature_model_parameters.keys())
+        if set(['a', 'b', 'deltaT']) <= params:
+            return self.sapm_temp
+        elif set(['u_c', 'u_v']) <= params:
+            return self.pvsyst_temp
+        else:
+            raise ValueError('could not infer temperature model from '
+                             'system.temperature_module_parameters {}.'
+                             .format(self.system.temperature_model_parameters))
 
     def sapm_temp(self):
-        self.temps = self.system.sapm_celltemp(self.total_irrad['poa_global'],
-                                               self.weather['wind_speed'],
-                                               self.weather['temp_air'])
+        self.cell_temperature = self.system.sapm_celltemp(
+            self.total_irrad['poa_global'], self.weather['temp_air'],
+            self.weather['wind_speed'])
+        return self
+
+    def pvsyst_temp(self):
+        self.cell_temperature = self.system.pvsyst_celltemp(
+            self.total_irrad['poa_global'], self.weather['temp_air'],
+            self.weather['wind_speed'])
         return self
 
     @property
@@ -918,7 +969,7 @@ class ModelChain(object):
         self
 
         Assigns attributes: times, solar_position, airmass, irradiance,
-        total_irrad, effective_irradiance, weather, temps, aoi,
+        total_irrad, effective_irradiance, weather, cell_temperature, aoi,
         aoi_modifier, spectral_modifier, dc, ac, losses.
         """
 
@@ -926,7 +977,7 @@ class ModelChain(object):
         self.aoi_model()
         self.spectral_model()
         self.effective_irradiance_model()
-        self.temp_model()
+        self.temperature_model()
         self.dc_model()
         self.losses_model()
         self.ac_model()
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
index 1a3969d..5a00907 100644
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -7,12 +7,17 @@ from collections import OrderedDict
 import io
 import os
 from urllib.request import urlopen
+import warnings
 import numpy as np
 import pandas as pd
 
-from pvlib import atmosphere, irradiance, tools, singlediode as _singlediode
-from pvlib.tools import _build_kwargs, cosd
+from pvlib._deprecation import deprecated
+
+from pvlib import (atmosphere, irradiance, singlediode as _singlediode,
+                   temperature)
+from pvlib.tools import _build_kwargs, cosd, asind, sind, tand
 from pvlib.location import Location
+from pvlib._deprecation import pvlibDeprecationWarning
 
 
 # a dict of required parameter names for each DC power model
@@ -40,16 +45,6 @@ DC_MODEL_PARAMS = {
 }
 
 
-TEMP_MODEL_PARAMS = {
-    'sapm': {'open_rack_cell_glassback': (-3.47, -.0594, 3),
-             'roof_mount_cell_glassback': (-2.98, -.0471, 1),
-             'open_rack_cell_polymerback': (-3.56, -.0750, 3),
-             'insulated_back_polymerback': (-2.81, -.0455, 0),
-             'open_rack_polymer_thinfilm_steel': (-3.58, -.113, 3),
-             '22x_concentrator_tracker': (-3.23, -.130, 13)},
-    'pvsyst': {'freestanding': (29.0, 0), 'insulated': (15.0, 0)}
-}
-
 # not sure if this belongs in the pvsystem module.
 # maybe something more like core.py? It may eventually grow to
 # import a lot more functionality from other modules.
@@ -107,9 +102,16 @@ class PVSystem(object):
         May be used to look up the module_parameters dictionary
         via some other method.
 
+    module_type : None or string, default 'glass_polymer'
+         Describes the module's construction. Valid strings are 'glass_polymer'
+         and 'glass_glass'. Used for cell and module temperature calculations.
+
     module_parameters : None, dict or Series, default None
         Module parameters as defined by the SAPM, CEC, or other.
 
+    temperature_model_parameters : None, dict or Series, default None.
+        Temperature model parameters as defined by the SAPM, Pvsyst, or other.
+
     modules_per_string: int or float, default 1
         See system topology discussion above.
 
@@ -124,8 +126,9 @@ class PVSystem(object):
     inverter_parameters : None, dict or Series, default None
         Inverter parameters as defined by the SAPM, CEC, or other.
 
-    racking_model : None or string, default 'open_rack_cell_glassback'
-        Used for cell and module temperature calculations.
+    racking_model : None or string, default 'open_rack'
+        Valid strings are 'open_rack', 'close_mount', and 'insulated_back'.
+        Used to identify a parameter set for the SAPM cell temperature model.
 
     losses_parameters : None, dict or Series, default None
         Losses parameters as defined by PVWatts or other.
@@ -146,13 +149,13 @@ class PVSystem(object):
     def __init__(self,
                  surface_tilt=0, surface_azimuth=180,
                  albedo=None, surface_type=None,
-                 module=None, module_parameters=None,
+                 module=None, module_type='glass_polymer',
+                 module_parameters=None,
+                 temperature_model_parameters=None,
                  modules_per_string=1, strings_per_inverter=1,
                  inverter=None, inverter_parameters=None,
-                 racking_model='open_rack_cell_glassback',
-                 losses_parameters=None, name=None, **kwargs):
-
-        self.name = name
+                 racking_model='open_rack', losses_parameters=None, name=None,
+                 **kwargs):
 
         self.surface_tilt = surface_tilt
         self.surface_azimuth = surface_azimuth
@@ -171,6 +174,31 @@ class PVSystem(object):
         else:
             self.module_parameters = module_parameters
 
+        self.module_type = module_type
+        self.racking_model = racking_model
+
+        if temperature_model_parameters is None:
+            self.temperature_model_parameters = \
+                self._infer_temperature_model_params()
+            # TODO: in v0.8 check if an empty dict is returned and raise error
+        else:
+            self.temperature_model_parameters = temperature_model_parameters
+
+        # TODO: deprecated behavior if PVSystem.temperature_model_parameters
+        # are not specified. Remove in v0.8
+        if not any(self.temperature_model_parameters):
+            warnings.warn(
+                'Required temperature_model_parameters is not specified '
+                'and parameters are not inferred from racking_model and '
+                'module_type. Reverting to deprecated default: SAPM cell '
+                'temperature model parameters for a glass/glass module in '
+                'open racking. In the future '
+                'PVSystem.temperature_model_parameters will be required',
+                pvlibDeprecationWarning)
+            params = temperature._temperature_model_params(
+                'sapm', 'open_rack_glass_glass')
+            self.temperature_model_parameters = params
+
         self.modules_per_string = modules_per_string
         self.strings_per_inverter = strings_per_inverter
 
@@ -185,7 +213,7 @@ class PVSystem(object):
         else:
             self.losses_parameters = losses_parameters
 
-        self.racking_model = racking_model
+        self.name = name
 
     def __repr__(self):
         attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module',
@@ -404,24 +432,15 @@ class PVSystem(object):
         """
         Use the :py:func:`sapm` function, the input parameters,
         and ``self.module_parameters`` to calculate
-        Voc, Isc, Ix, Ixx, Vmp/Imp.
+        Voc, Isc, Ix, Ixx, Vmp, and Imp.
 
         Parameters
         ----------
-        poa_direct : Series
-            The direct irradiance incident upon the module (W/m^2).
-
-        poa_diffuse : Series
-            The diffuse irradiance incident on module.
-
-        temp_cell : Series
-            The cell temperature (degrees C).
-
-        airmass_absolute : Series
-            Absolute airmass.
+        effective_irradiance : numeric
+            The irradiance (W/m2) that is converted to photocurrent.
 
-        aoi : Series
-            Angle of incidence (degrees).
+        temp_cell : float or Series
+            The average cell temperature of cells within a module in C.
 
         kwargs
             See pvsystem.sapm for details
@@ -432,20 +451,44 @@ class PVSystem(object):
         """
         return sapm(effective_irradiance, temp_cell, self.module_parameters)
 
-    def sapm_celltemp(self, irrad, wind, temp):
-        """Uses :py:func:`sapm_celltemp` to calculate module and cell
-        temperatures based on ``self.racking_model`` and
-        the input parameters.
+    def sapm_celltemp(self, poa_global, temp_air, wind_speed):
+        """Uses :py:func:`temperature.sapm_cell` to calculate cell
+        temperatures.
 
         Parameters
         ----------
-        See pvsystem.sapm_celltemp for details
+        poa_global : numeric
+            Total incident irradiance in W/m^2.
+
+        temp_air : numeric
+            Ambient dry bulb temperature in degrees C.
+
+        wind_speed : numeric
+            Wind speed in m/s at a height of 10 meters.
 
         Returns
         -------
-        See pvsystem.sapm_celltemp for details
+        numeric, values in degrees C.
         """
-        return sapm_celltemp(irrad, wind, temp, self.racking_model)
+        kwargs = _build_kwargs(['a', 'b', 'deltaT'],
+                               self.temperature_model_parameters)
+        return temperature.sapm_cell(poa_global, temp_air, wind_speed,
+                                     **kwargs)
+
+    def _infer_temperature_model_params(self):
+        # try to infer temperature model parameters from from racking_model
+        # and module_type
+        param_set = self.racking_model + '_' + self.module_type
+        if param_set in temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']:
+            return temperature._temperature_model_params('sapm', param_set)
+        elif 'freestanding' in param_set:
+            return temperature._temperature_model_params('pvsyst',
+                                                         'freestanding')
+        elif 'insulated' in param_set:  # after SAPM to avoid confusing keys
+            return temperature._temperature_model_params('pvsyst',
+                                                         'insulated')
+        else:
+            return {}
 
     def sapm_spectral_loss(self, airmass_absolute):
         """
@@ -516,21 +559,39 @@ class PVSystem(object):
             self.module_parameters, reference_irradiance=reference_irradiance)
 
     def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0):
-        """Uses :py:func:`pvsyst_celltemp` to calculate module temperatures
-        based on ``self.racking_model`` and the input parameters.
+        """Uses :py:func:`temperature.pvsyst_cell` to calculate cell
+        temperature.
 
         Parameters
         ----------
-        See pvsystem.pvsyst_celltemp for details
+        poa_global : numeric
+            Total incident irradiance in W/m^2.
+
+        temp_air : numeric
+            Ambient dry bulb temperature in degrees C.
+
+        wind_speed : numeric, default 1.0
+            Wind speed in m/s measured at the same height for which the wind
+            loss factor was determined.  The default value is 1.0, which is
+            the wind speed at module height used to determine NOCT.
+
+        eta_m : numeric, default 0.1
+            Module external efficiency as a fraction, i.e.,
+            DC power / poa_global.
+
+        alpha_absorption : numeric, default 0.9
+            Absorption coefficient
 
         Returns
         -------
-        See pvsystem.pvsyst_celltemp for details
+        numeric, values in degrees C.
         """
         kwargs = _build_kwargs(['eta_m', 'alpha_absorption'],
                                self.module_parameters)
-        return pvsyst_celltemp(poa_global, temp_air, wind_speed,
-                               model_params=self.racking_model, **kwargs)
+        kwargs.update(_build_kwargs(['u_c', 'u_v'],
+                                    self.temperature_model_parameters))
+        return temperature.pvsyst_cell(poa_global, temp_air, wind_speed,
+                                       **kwargs)
 
     def first_solar_spectral_loss(self, pw, airmass_absolute):
 
@@ -1020,20 +1081,18 @@ def physicaliam(aoi, n=1.526, K=4., L=0.002):
     aoi = np.where(aoi == 0, zeroang, aoi)
 
     # angle of reflection
-    thetar_deg = tools.asind(1.0 / n*(tools.sind(aoi)))
+    thetar_deg = asind(1.0 / n * (sind(aoi)))
 
     # reflectance and transmittance for normal incidence light
     rho_zero = ((1-n) / (1+n)) ** 2
     tau_zero = np.exp(-K*L)
 
     # reflectance for parallel and perpendicular polarized light
-    rho_para = (tools.tand(thetar_deg - aoi) /
-                tools.tand(thetar_deg + aoi)) ** 2
-    rho_perp = (tools.sind(thetar_deg - aoi) /
-                tools.sind(thetar_deg + aoi)) ** 2
+    rho_para = (tand(thetar_deg - aoi) / tand(thetar_deg + aoi)) ** 2
+    rho_perp = (sind(thetar_deg - aoi) / sind(thetar_deg + aoi)) ** 2
 
     # transmittance for non-normal light
-    tau = np.exp(-K*L / tools.cosd(thetar_deg))
+    tau = np.exp(-K * L / cosd(thetar_deg))
 
     # iam is ratio of non-normal to normal incidence transmitted light
     # after deducting the reflected portion of each
@@ -1901,7 +1960,8 @@ def sapm(effective_irradiance, temp_cell, module):
     See Also
     --------
     retrieve_sam
-    sapm_celltemp
+    temperature.sapm_cell
+    temperature.sapm_module
     '''
 
     T0 = 25
@@ -1965,174 +2025,83 @@ def sapm(effective_irradiance, temp_cell, module):
     return out
 
 
-def sapm_celltemp(poa_global, wind_speed, temp_air,
-                  model='open_rack_cell_glassback'):
-    '''
-    Estimate cell and module temperatures per the Sandia PV Array
-    Performance Model (SAPM, SAND2004-3535), from the incident
-    irradiance, wind speed, ambient temperature, and SAPM module
-    parameters.
-
-    Parameters
-    ----------
-    poa_global : float or Series
-        Total incident irradiance in W/m^2.
-
-    wind_speed : float or Series
-        Wind speed in m/s at a height of 10 meters.
-
-    temp_air : float or Series
-        Ambient dry bulb temperature in degrees C.
-
-    model : string, list, or dict, default 'open_rack_cell_glassback'
-        Model to be used.
-
-        If string, can be:
-
-            * 'open_rack_cell_glassback' (default)
-            * 'roof_mount_cell_glassback'
-            * 'open_rack_cell_polymerback'
-            * 'insulated_back_polymerback'
-            * 'open_rack_polymer_thinfilm_steel'
-            * '22x_concentrator_tracker'
-
-        If dict, supply the following parameters
-        (if list, in the following order):
-
-            * a : float
-                SAPM module parameter for establishing the upper
-                limit for module temperature at low wind speeds and
-                high solar irradiance.
-
-            * b : float
-                SAPM module parameter for establishing the rate at
-                which the module temperature drops as wind speed increases
-                (see SAPM eqn. 11).
-
-            * deltaT : float
-                SAPM module parameter giving the temperature difference
-                between the cell and module back surface at the
-                reference irradiance, E0.
-
-    Returns
-    --------
-    DataFrame with columns 'temp_cell' and 'temp_module'.
-    Values in degrees C.
-
-    References
-    ----------
-    [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance
-    Model", SAND Report 3535, Sandia National Laboratories, Albuquerque,
-    NM.
-
-    See Also
-    --------
-    sapm
-    '''
-
-    temp_models = TEMP_MODEL_PARAMS['sapm']
-
-    if isinstance(model, str):
-        model = temp_models[model.lower()]
-
-    elif isinstance(model, (dict, pd.Series)):
-        model = [model['a'], model['b'], model['deltaT']]
-
-    a = model[0]
-    b = model[1]
-    deltaT = model[2]
-
-    E0 = 1000.  # Reference irradiance
-
-    temp_module = pd.Series(poa_global * np.exp(a + b * wind_speed) + temp_air)
-
-    temp_cell = temp_module + (poa_global / E0) * (deltaT)
-
-    return pd.DataFrame({'temp_cell': temp_cell, 'temp_module': temp_module})
-
-
-def pvsyst_celltemp(poa_global, temp_air, wind_speed=1.0, eta_m=0.1,
-                    alpha_absorption=0.9, model_params='freestanding'):
-    """
-    Calculate cell temperature using an emperical heat loss factor model
-    as implemented in PVsyst.
-
-    The heat loss factors provided through the 'model_params' argument
-    represent the combined effect of convection, radiation and conduction,
-    and their values are experimentally determined.
-
-    Parameters
-    ----------
-    poa_global : numeric
-        Total incident irradiance in W/m^2.
-
-    temp_air : numeric
-        Ambient dry bulb temperature in degrees C.
-
-    wind_speed : numeric, default 1.0
-        Wind speed in m/s measured at the same height for which the wind loss
-        factor was determined.  The default value is 1.0, which is the wind
-        speed at module height used to determine NOCT.
-
-    eta_m : numeric, default 0.1
-        Module external efficiency as a fraction, i.e., DC power / poa_global.
-
-    alpha_absorption : numeric, default 0.9
-        Absorption coefficient
-
-    model_params : string, tuple, or list (no dict), default 'freestanding'
-        Heat loss factors to be used.
-
-        If string, can be:
-
-            * 'freestanding' (default)
-                Modules with rear surfaces exposed to open air (e.g. rack
-                mounted).
-            * 'insulated'
-                Modules with rear surfaces in close proximity to another
-                surface (e.g. roof mounted).
-
-        If tuple/list, supply parameters in the following order:
-
-            * constant_loss_factor : float
-                Combined heat loss factor coefficient. Freestanding
-                default is 29, fully insulated arrays is 15.
-
-            * wind_loss_factor : float
-                Combined heat loss factor influenced by wind. Default is 0.
-
-    Returns
-    -------
-    temp_cell : numeric or Series
-        Cell temperature in degrees Celsius
-
-    References
-    ----------
-    [1]"PVsyst 6 Help", Files.pvsyst.com, 2018. [Online]. Available:
-    http://files.pvsyst.com/help/index.html. [Accessed: 10- Dec- 2018].
-
-    [2] Faiman, D. (2008). "Assessing the outdoor operating temperature of
-    photovoltaic modules." Progress in Photovoltaics 16(4): 307-315.
-    """
-
-    pvsyst_presets = TEMP_MODEL_PARAMS['pvsyst']
-
-    if isinstance(model_params, str):
-        model_params = model_params.lower()
-        constant_loss_factor, wind_loss_factor = pvsyst_presets[model_params]
-    elif isinstance(model_params, (tuple, list)):
-        constant_loss_factor, wind_loss_factor = model_params
-    else:
-        raise TypeError(
-            "Please provide model_params as a str, or tuple/list."
-        )
-
-    total_loss_factor = wind_loss_factor * wind_speed + constant_loss_factor
-    heat_input = poa_global * alpha_absorption * (1 - eta_m)
-    temp_difference = heat_input / total_loss_factor
-    temp_cell = temp_air + temp_difference
-
-    return temp_cell
+def _sapm_celltemp_translator(*args, **kwargs):
+    # TODO: remove this function after deprecation period for sapm_celltemp
+    new_kwargs = {}
+    # convert position arguments to kwargs
+    old_arg_list = ['poa_global', 'wind_speed', 'temp_air', 'model']
+    for pos in range(len(args)):
+        new_kwargs[old_arg_list[pos]] = args[pos]
+    # determine value for new kwarg 'model'
+    try:
+        param_set = new_kwargs['model']
+        new_kwargs.pop('model')  # model is not a new kwarg
+    except KeyError:
+        # 'model' not in positional arguments, check kwargs
+        try:
+            param_set = kwargs['model']
+            kwargs.pop('model')
+        except KeyError:
+            # 'model' not in kwargs, use old default value
+            param_set = 'open_rack_glass_glass'
+    if type(param_set) is list:
+        new_kwargs.update({'a': param_set[0],
+                           'b': param_set[1],
+                           'deltaT': param_set[2]})
+    elif type(param_set) is dict:
+        new_kwargs.update(param_set)
+    else:  # string
+        params = temperature._temperature_model_params('sapm', param_set)
+        new_kwargs.update(params)
+    new_kwargs.update(kwargs)  # kwargs with unchanged names
+    new_kwargs['irrad_ref'] = 1000  # default for new kwarg
+    # convert old positional arguments to named kwargs
+    return temperature.sapm_cell(**new_kwargs)
+
+
+sapm_celltemp = deprecated('0.7', alternative='temperature.sapm_cell',
+                           name='sapm_celltemp', removal='0.8',
+                           addendum='Note that the arguments and argument '
+                           'order for temperature.sapm_cell are different '
+                           'than for sapm_celltemp')(_sapm_celltemp_translator)
+
+
+def _pvsyst_celltemp_translator(*args, **kwargs):
+    # TODO: remove this function after deprecation period for pvsyst_celltemp
+    new_kwargs = {}
+    # convert position arguments to kwargs
+    old_arg_list = ['poa_global', 'temp_air', 'wind_speed', 'eta_m',
+                    'alpha_absorption', 'model_params']
+    for pos in range(len(args)):
+        new_kwargs[old_arg_list[pos]] = args[pos]
+    # determine value for new kwarg 'model'
+    try:
+        param_set = new_kwargs['model_params']
+        new_kwargs.pop('model_params')  # model_params is not a new kwarg
+    except KeyError:
+        # 'model_params' not in positional arguments, check kwargs
+        try:
+            param_set = kwargs['model_params']
+            kwargs.pop('model_params')
+        except KeyError:
+            # 'model_params' not in kwargs, use old default value
+            param_set = 'freestanding'
+    if type(param_set) in (list, tuple):
+        new_kwargs.update({'u_c': param_set[0],
+                           'u_v': param_set[1]})
+    else:  # string
+        params = temperature._temperature_model_params('pvsyst', param_set)
+        new_kwargs.update(params)
+    new_kwargs.update(kwargs)  # kwargs with unchanged names
+    # convert old positional arguments to named kwargs
+    return temperature.pvsyst_cell(**new_kwargs)
+
+
+pvsyst_celltemp = deprecated(
+    '0.7', alternative='temperature.pvsyst_cell', name='pvsyst_celltemp',
+    removal='0.8', addendum='Note that the argument names for '
+    'temperature.pvsyst_cell are different than '
+    'for pvsyst_celltemp')(_pvsyst_celltemp_translator)
 
 
 def sapm_spectral_loss(airmass_absolute, module):
diff --git a/pvlib/temperature.py b/pvlib/temperature.py
new file mode 100644
index 0000000..f43d372
--- /dev/null
+++ b/pvlib/temperature.py
@@ -0,0 +1,271 @@
+"""
+The ``temperature`` module contains functions for modeling temperature of
+PV modules and cells.
+"""
+
+import numpy as np
+
+
+TEMPERATURE_MODEL_PARAMETERS = {
+    'sapm': {
+        'open_rack_glass_glass': {'a': -3.47, 'b': -.0594, 'deltaT': 3},
+        'close_mount_glass_glass': {'a': -2.98, 'b': -.0471, 'deltaT': 1},
+        'open_rack_glass_polymer': {'a': -3.56, 'b': -.0750, 'deltaT': 3},
+        'insulated_back_glass_polymer': {'a': -2.81, 'b': -.0455, 'deltaT': 0},
+    },
+    'pvsyst': {'freestanding': {'u_c': 29.0, 'u_v': 0},
+               'insulated': {'u_c': 15.0, 'u_v': 0}}
+}
+
+
+def _temperature_model_params(model, parameter_set):
+    try:
+        params = TEMPERATURE_MODEL_PARAMETERS[model]
+        return params[parameter_set]
+    except KeyError:
+        msg = ('{} is not a named set of parameters for the {} cell'
+               ' temperature model.'
+               ' See pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS'
+               ' for names'.format(parameter_set, model))
+        raise KeyError(msg)
+
+
+def sapm_cell(poa_global, temp_air, wind_speed, a, b, deltaT,
+              irrad_ref=1000):
+    r'''
+    Calculate cell temperature per the Sandia PV Array Performance Model [1].
+
+    Parameters
+    ----------
+    poa_global : numeric
+        Total incident irradiance [W/m^2].
+
+    temp_air : numeric
+        Ambient dry bulb temperature [C].
+
+    wind_speed : numeric
+        Wind speed at a height of 10 meters [m/s].
+
+    a : float
+        Parameter :math:`a` in :eq:`sapm1`.
+
+    b : float
+        Parameter :math:`b` in :eq:`sapm1`.
+
+    deltaT : float
+        Parameter :math:`\Delta T` in :eq:`sapm2` [C].
+
+    irrad_ref : float, default 1000
+        Reference irradiance, parameter :math:`E_{0}` in
+        :eq:`sapm2` [W/m^2].
+
+    Returns
+    -------
+    numeric, values in degrees C.
+
+    Notes
+    -----
+    The model for cell temperature :math:`T_{C}` is given by a pair of
+    equations (Eq. 11 and 12 in [1]).
+
+    .. math::
+       :label: sapm1
+
+       T_{m} = E \times \exp (a + b \times WS) + T_{a}
+
+    .. math::
+       :label: sapm2
+
+       T_{C} = T_{m} + \frac{E}{E_{0}} \Delta T
+
+    The module back surface temperature :math:`T_{m}` is implemented in
+    :py:func:`~pvlib.temperature.sapm_module`.
+
+    Inputs to the model are plane-of-array irradiance :math:`E` (W/m2) and
+    ambient air temperature :math:`T_{a}` (C). Model parameters depend both on
+    the module construction and its mounting. Parameter sets are provided in
+    [1] for representative modules and mounting, and are coded for convenience
+    in ``pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS``.
+
+    +---------------+----------------+-------+---------+---------------------+
+    | Module        | Mounting       | a     | b       | :math:`\Delta T [C]`|
+    +===============+================+=======+=========+=====================+
+    | glass/glass   | open rack      | -3.47 | -0.0594 | 3                   |
+    +---------------+----------------+-------+---------+---------------------+
+    | glass/glass   | close roof     | -2.98 | -0.0471 | 1                   |
+    +---------------+----------------+-------+---------+---------------------+
+    | glass/polymer | open rack      | -3.56 | -0.075  | 3                   |
+    +---------------+----------------+-------+---------+---------------------+
+    | glass/polymer | insulated back | -2.81 | -0.0455 | 0                   |
+    +---------------+----------------+-------+---------+---------------------+
+
+    References
+    ----------
+    [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance
+    Model", SAND Report 3535, Sandia National Laboratories, Albuquerque,
+    NM.
+
+    Examples
+    --------
+    >>> from pvlib.temperature import sapm_cell, TEMPERATURE_MODEL_PARAMETERS
+    >>> params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']
+    >>> sapm_cell(1000, 10, 0, **params)
+    44.11703066106086
+    '''
+    module_temperature = sapm_module(poa_global, temp_air, wind_speed,
+                                     a, b)
+    return module_temperature + (poa_global / irrad_ref) * deltaT
+
+
+def sapm_module(poa_global, temp_air, wind_speed, a, b):
+    r'''
+    Calculate module back surface temperature per the Sandia PV Array
+    Performance Model [1].
+
+    Parameters
+    ----------
+    poa_global : numeric
+        Total incident irradiance [W/m^2].
+
+    temp_air : numeric
+        Ambient dry bulb temperature [C].
+
+    wind_speed : numeric
+        Wind speed at a height of 10 meters [m/s].
+
+    a : float
+        Parameter :math:`a` in :eq:`sapm1mod`.
+
+    b : float
+        Parameter :math:`b` in :eq:`sapm1mod`.
+
+    Returns
+    -------
+    numeric, values in degrees C.
+
+    Notes
+    -----
+    The model for module temperature :math:`T_{m}` is given by Eq. 11 in [1].
+
+    .. math::
+       :label: sapm1mod
+
+       T_{m} = E \times \exp (a + b \times WS) + T_{a}
+
+    Inputs to the model are plane-of-array irradiance :math:`E` (W/m2) and
+    ambient air temperature :math:`T_{a}` (C). Model outputs are surface
+    temperature at the back of the module :math:`T_{m}` and cell temperature
+    :math:`T_{C}`. Model parameters depend both on the module construction and
+    its mounting. Parameter sets are provided in [1] for representative modules
+    and mounting, and are coded for convenience in
+    ``temperature.TEMPERATURE_MODEL_PARAMETERS``.
+
+    +---------------+----------------+-------+---------+---------------------+
+    | Module        | Mounting       | a     | b       | :math:`\Delta T [C]`|
+    +===============+================+=======+=========+=====================+
+    | glass/glass   | open rack      | -3.47 | -0.0594 | 3                   |
+    +---------------+----------------+-------+---------+---------------------+
+    | glass/glass   | close roof     | -2.98 | -0.0471 | 1                   |
+    +---------------+----------------+-------+---------+---------------------+
+    | glass/polymer | open rack      | -3.56 | -0.075  | 3                   |
+    +---------------+----------------+-------+---------+---------------------+
+    | glass/polymer | insulated back | -2.81 | -0.0455 | 0                   |
+    +---------------+----------------+-------+---------+---------------------+
+
+    References
+    ----------
+    [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance
+    Model", SAND Report 3535, Sandia National Laboratories, Albuquerque,
+    NM.
+
+    '''
+    return poa_global * np.exp(a + b * wind_speed) + temp_air
+
+
+def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0,
+                eta_m=0.1, alpha_absorption=0.9):
+    r"""
+    Calculate cell temperature using an empirical heat loss factor model
+    as implemented in PVsyst.
+
+    Parameters
+    ----------
+    poa_global : numeric
+        Total incident irradiance [W/m^2].
+
+    temp_air : numeric
+        Ambient dry bulb temperature [C].
+
+    wind_speed : numeric, default 1.0
+        Wind speed in m/s measured at the same height for which the wind loss
+        factor was determined.  The default value 1.0 m/2 is the wind
+        speed at module height used to determine NOCT. [m/s]
+
+    u_c : float, default 29.0
+        Combined heat loss factor coefficient. The default value is
+        representative of freestanding modules with the rear surfaces exposed
+        to open air (e.g., rack mounted). Parameter :math:`U_{c}` in
+        :eq:`pvsyst` [W/(m^2 C)].
+
+    u_v : float, default 0.0
+        Combined heat loss factor influenced by wind. Parameter :math:`U_{v}`
+        in :eq:`pvsyst` [(W/m^2 C)(m/s)].
+
+    eta_m : numeric, default 0.1
+        Module external efficiency as a fraction, i.e., DC power / poa_global.
+        Parameter :math:`\eta_{m}` in :eq:`pvsyst`.
+
+    alpha_absorption : numeric, default 0.9
+        Absorption coefficient. Parameter :math:`\alpha` in :eq:`pvsyst`.
+
+    Returns
+    -------
+    numeric, values in degrees Celsius
+
+    Notes
+    -----
+    The Pvsyst model for cell temperature :math:`T_{C}` is given by
+
+    .. math::
+       :label: pvsyst
+
+        T_{C} = T_{a} + \frac{\alpha E (1 - \eta_{m})}{U_{c} + U_{v} \times WS}
+
+    Inputs to the model are plane-of-array irradiance :math:`E` (W/m2), ambient
+    air temperature :math:`T_{a}` (C) and wind speed :math:`WS` (m/s). Model
+    output is cell temperature :math:`T_{C}`. Model parameters depend both on
+    the module construction and its mounting. Parameters are provided in
+    [1] for open (freestanding) and close (insulated) mounting configurations,
+    , and are coded for convenience in
+    ``temperature.TEMPERATURE_MODEL_PARAMETERS``. The heat loss factors
+    provided represent the combined effect of convection, radiation and
+    conduction, and their values are experimentally determined.
+
+    +--------------+---------------+---------------+
+    | Mounting     | :math:`U_{c}` | :math:`U_{v}` |
+    +==============+===============+===============+
+    | freestanding | 29.0          | 0.0           |
+    +--------------+---------------+---------------+
+    | insulated    | 15.0          | 0.0           |
+    +--------------+---------------+---------------+
+
+    References
+    ----------
+    [1]"PVsyst 6 Help", Files.pvsyst.com, 2018. [Online]. Available:
+    http://files.pvsyst.com/help/index.html. [Accessed: 10- Dec- 2018].
+
+    [2] Faiman, D. (2008). "Assessing the outdoor operating temperature of
+    photovoltaic modules." Progress in Photovoltaics 16(4): 307-315.
+
+    Examples
+    --------
+    >>> from pvlib.temperature import pvsyst_cell, TEMPERATURE_MODEL_PARAMETERS
+    >>> params = TEMPERATURE_MODEL_PARAMETERS['pvsyst']['freestanding']
+    >>> pvsyst_cell(1000, 10, **params)
+    37.93103448275862
+    """
+
+    total_loss_factor = u_c + u_v * wind_speed
+    heat_input = poa_global * alpha_absorption * (1 - eta_m)
+    temp_difference = heat_input / total_loss_factor
+    return temp_air + temp_difference


Test patch:
diff --git a/pvlib/test/conftest.py b/pvlib/test/conftest.py
index ee50b52..1c690c2 100644
--- a/pvlib/test/conftest.py
+++ b/pvlib/test/conftest.py
@@ -349,3 +349,10 @@ def cec_module_fs_495():
         'Technology': 'CdTe',
     }
     return parameters
+
+
+@pytest.fixture(scope='function')
+def sapm_temperature_cs5p_220m():
+    # SAPM temperature model parameters for Canadian_Solar_CS5P_220M
+    # (glass/polymer) in open rack
+    return {'a': -3.40641, 'b': -0.0842075, 'deltaT': 3}
diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py
index fd2ef40..20a8a95 100644
--- a/pvlib/test/test_modelchain.py
+++ b/pvlib/test/test_modelchain.py
@@ -3,7 +3,7 @@ import sys
 import numpy as np
 import pandas as pd
 
-from pvlib import modelchain, pvsystem
+from pvlib import modelchain, pvsystem, temperature
 from pvlib.modelchain import ModelChain
 from pvlib.pvsystem import PVSystem
 from pvlib.tracking import SingleAxisTracker
@@ -17,63 +17,77 @@ from conftest import fail_on_pvlib_version, requires_scipy, requires_tables
 
 
 @pytest.fixture
-def system(sam_data, cec_inverter_parameters):
+def system(sam_data, cec_inverter_parameters, sapm_temperature_cs5p_220m):
     modules = sam_data['sandiamod']
     module = 'Canadian_Solar_CS5P_220M___2009_'
     module_parameters = modules[module].copy()
+    temp_model_params = sapm_temperature_cs5p_220m.copy()
     system = PVSystem(surface_tilt=32.2, surface_azimuth=180,
                       module=module,
                       module_parameters=module_parameters,
+                      temperature_model_parameters=temp_model_params,
                       inverter_parameters=cec_inverter_parameters)
     return system
 
 
 @pytest.fixture
-def cec_dc_snl_ac_system(cec_module_cs5p_220m, cec_inverter_parameters):
+def cec_dc_snl_ac_system(cec_module_cs5p_220m, cec_inverter_parameters,
+                         sapm_temperature_cs5p_220m):
     module_parameters = cec_module_cs5p_220m.copy()
     module_parameters['b'] = 0.05
     module_parameters['EgRef'] = 1.121
     module_parameters['dEgdT'] = -0.0002677
+    temp_model_params = sapm_temperature_cs5p_220m.copy()
     system = PVSystem(surface_tilt=32.2, surface_azimuth=180,
                       module=module_parameters['Name'],
                       module_parameters=module_parameters,
+                      temperature_model_parameters=temp_model_params,
                       inverter_parameters=cec_inverter_parameters)
     return system
 
 
 @pytest.fixture
-def cec_dc_native_snl_ac_system(cec_module_cs5p_220m, cec_inverter_parameters):
+def cec_dc_native_snl_ac_system(cec_module_cs5p_220m, cec_inverter_parameters,
+                                sapm_temperature_cs5p_220m):
     module_parameters = cec_module_cs5p_220m.copy()
+    temp_model_params = sapm_temperature_cs5p_220m.copy()
     system = PVSystem(surface_tilt=32.2, surface_azimuth=180,
                       module=module_parameters['Name'],
                       module_parameters=module_parameters,
+                      temperature_model_parameters=temp_model_params,
                       inverter_parameters=cec_inverter_parameters)
     return system
 
 
 @pytest.fixture
-def pvsyst_dc_snl_ac_system(pvsyst_module_params, cec_inverter_parameters):
+def pvsyst_dc_snl_ac_system(pvsyst_module_params, cec_inverter_parameters,
+                            sapm_temperature_cs5p_220m):
     module = 'PVsyst test module'
     module_parameters = pvsyst_module_params
     module_parameters['b'] = 0.05
+    temp_model_params = sapm_temperature_cs5p_220m.copy()
     system = PVSystem(surface_tilt=32.2, surface_azimuth=180,
                       module=module,
                       module_parameters=module_parameters,
+                      temperature_model_parameters=temp_model_params,
                       inverter_parameters=cec_inverter_parameters)
     return system
 
 
 @pytest.fixture
-def cec_dc_adr_ac_system(sam_data, cec_module_cs5p_220m):
+def cec_dc_adr_ac_system(sam_data, cec_module_cs5p_220m,
+                         sapm_temperature_cs5p_220m):
     module_parameters = cec_module_cs5p_220m.copy()
     module_parameters['b'] = 0.05
     module_parameters['EgRef'] = 1.121
     module_parameters['dEgdT'] = -0.0002677
+    temp_model_params = sapm_temperature_cs5p_220m.copy()
     inverters = sam_data['adrinverter']
     inverter = inverters['Zigor__Sunzet_3_TL_US_240V__CEC_2011_'].copy()
     system = PVSystem(surface_tilt=32.2, surface_azimuth=180,
                       module=module_parameters['Name'],
                       module_parameters=module_parameters,
+                      temperature_model_parameters=temp_model_params,
                       inverter_parameters=inverter)
     return system
 
@@ -88,11 +102,13 @@ def pvwatts_dc_snl_ac_system(cec_inverter_parameters):
 
 
 @pytest.fixture
-def pvwatts_dc_pvwatts_ac_system(sam_data):
+def pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m):
     module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003}
+    temp_model_params = sapm_temperature_cs5p_220m.copy()
     inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95}
     system = PVSystem(surface_tilt=32.2, surface_azimuth=180,
                       module_parameters=module_parameters,
+                      temperature_model_parameters=temp_model_params,
                       inverter_parameters=inverter_parameters)
     return system
 
@@ -136,7 +152,7 @@ def test_run_model(system, location):
     with pytest.warns(pvlibDeprecationWarning):
         ac = mc.run_model(times).ac
 
-    expected = pd.Series(np.array([  183.522449305,  -2.00000000e-02]),
+    expected = pd.Series(np.array([183.522449305, -2.00000000e-02]),
                          index=times)
     assert_series_equal(ac, expected, check_less_precise=1)
 
@@ -148,7 +164,7 @@ def test_run_model_with_irradiance(system, location):
                               index=times)
     ac = mc.run_model(times, weather=irradiance).ac
 
-    expected = pd.Series(np.array([  1.90054749e+02,  -2.00000000e-02]),
+    expected = pd.Series(np.array([187.80746495, -2.00000000e-02]),
                          index=times)
     assert_series_equal(ac, expected)
 
@@ -160,7 +176,7 @@ def test_run_model_perez(system, location):
                               index=times)
     ac = mc.run_model(times, weather=irradiance).ac
 
-    expected = pd.Series(np.array([  190.194545796,  -2.00000000e-02]),
+    expected = pd.Series(np.array([187.94295642, -2.00000000e-02]),
                          index=times)
     assert_series_equal(ac, expected)
 
@@ -173,28 +189,46 @@ def test_run_model_gueymard_perez(system, location):
                               index=times)
     ac = mc.run_model(times, weather=irradiance).ac
 
-    expected = pd.Series(np.array([  190.194760203,  -2.00000000e-02]),
+    expected = pd.Series(np.array([187.94317405, -2.00000000e-02]),
                          index=times)
     assert_series_equal(ac, expected)
 
 
 def test_run_model_with_weather(system, location, weather, mocker):
-    mc = ModelChain(system, location)
-    m = mocker.spy(system, 'sapm_celltemp')
     weather['wind_speed'] = 5
     weather['temp_air'] = 10
+    # test with sapm cell temperature model
+    system.racking_model = 'open_rack'
+    system.module_type = 'glass_glass'
+    mc = ModelChain(system, location)
+    mc.temperature_model = 'sapm'
+    m_sapm = mocker.spy(system, 'sapm_celltemp')
     mc.run_model(weather.index, weather=weather)
-    assert m.call_count == 1
+    assert m_sapm.call_count == 1
     # assert_called_once_with cannot be used with series, so need to use
     # assert_series_equal on call_args
-    assert_series_equal(m.call_args[0][1], weather['wind_speed'])  # wind
-    assert_series_equal(m.call_args[0][2], weather['temp_air'])  # temp
+    assert_series_equal(m_sapm.call_args[0][1], weather['temp_air'])  # temp
+    assert_series_equal(m_sapm.call_args[0][2], weather['wind_speed'])  # wind
+    assert not mc.ac.empty
+    # test with pvsyst cell temperature model
+    system.racking_model = 'freestanding'
+    system.temperature_model_parameters = \
+        temperature._temperature_model_params('pvsyst', 'freestanding')
+    mc = ModelChain(system, location)
+    mc.temperature_model = 'pvsyst'
+    m_pvsyst = mocker.spy(system, 'pvsyst_celltemp')
+    mc.run_model(weather.index, weather=weather)
+    assert m_pvsyst.call_count == 1
+    assert_series_equal(m_pvsyst.call_args[0][1], weather['temp_air'])
+    assert_series_equal(m_pvsyst.call_args[0][2], weather['wind_speed'])
     assert not mc.ac.empty
 
 
 def test_run_model_tracker(system, location, weather, mocker):
-    system = SingleAxisTracker(module_parameters=system.module_parameters,
-                               inverter_parameters=system.inverter_parameters)
+    system = SingleAxisTracker(
+        module_parameters=system.module_parameters,
+        temperature_model_parameters=system.temperature_model_parameters,
+        inverter_parameters=system.inverter_parameters)
     mocker.spy(system, 'singleaxis')
     mc = ModelChain(system, location)
     mc.run_model(weather.index, weather=weather)
@@ -232,13 +266,24 @@ def test_infer_dc_model(system, cec_dc_snl_ac_system, pvsyst_dc_snl_ac_system,
                          'pvsyst': 'calcparams_pvsyst',
                          'singlediode': 'calcparams_desoto',
                          'pvwatts_dc': 'pvwatts_dc'}
+    temp_model_function = {'sapm': 'sapm',
+                           'cec': 'sapm',
+                           'desoto': 'sapm',
+                           'pvsyst': 'pvsyst',
+                           'singlediode': 'sapm',
+                           'pvwatts_dc': 'sapm'}
+    temp_model_params = {'sapm': {'a': -3.40641, 'b': -0.0842075, 'deltaT': 3},
+                         'pvsyst': {'u_c': 29.0, 'u_v': 0}}
     system = dc_systems[dc_model]
+    system.temperature_model_parameters = temp_model_params[
+        temp_model_function[dc_model]]
     # remove Adjust from model parameters for desoto, singlediode
     if dc_model in ['desoto', 'singlediode']:
         system.module_parameters.pop('Adjust')
     m = mocker.spy(system, dc_model_function[dc_model])
     mc = ModelChain(system, location,
-                    aoi_model='no_loss', spectral_model='no_loss')
+                    aoi_model='no_loss', spectral_model='no_loss',
+                    temperature_model=temp_model_function[dc_model])
     mc.run_model(weather.index, weather=weather)
     assert m.call_count == 1
     assert isinstance(mc.dc, (pd.Series, pd.DataFrame))
@@ -259,6 +304,35 @@ def test_infer_spectral_model(location, system, cec_dc_snl_ac_system,
     assert isinstance(mc, ModelChain)
 
 
+@pytest.mark.parametrize('temp_model', [
+    'sapm', pytest.param('pvsyst', marks=requires_scipy)])
+def test_infer_temp_model(location, system, pvsyst_dc_snl_ac_system,
+                          temp_model):
+    dc_systems = {'sapm': system,
+                  'pvsyst': pvsyst_dc_snl_ac_system}
+    system = dc_systems[temp_model]
+    mc = ModelChain(system, location,
+                    orientation_strategy='None', aoi_model='physical',
+                    spectral_model='no_loss')
+    assert isinstance(mc, ModelChain)
+
+
+@requires_scipy
+def test_infer_temp_model_invalid(location, system):
+    system.temperature_model_parameters.pop('a')
+    with pytest.raises(ValueError):
+        ModelChain(system, location, orientation_strategy='None',
+                   aoi_model='physical', spectral_model='no_loss')
+
+
+@requires_scipy
+def test_temperature_model_inconsistent(location, system):
+    with pytest.raises(ValueError):
+        ModelChain(system, location, orientation_strategy='None',
+                   aoi_model='physical', spectral_model='no_loss',
+                   temperature_model='pvsyst')
+
+
 def test_dc_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather,
                             mocker):
     m = mocker.spy(sys.modules[__name__], 'poadc')
@@ -422,7 +496,7 @@ def test_invalid_dc_model_params(system, cec_dc_snl_ac_system,
                                  pvwatts_dc_pvwatts_ac_system, location):
     kwargs = {'dc_model': 'sapm', 'ac_model': 'snlinverter',
               'aoi_model': 'no_loss', 'spectral_model': 'no_loss',
-              'temp_model': 'sapm', 'losses_model': 'no_loss'}
+              'temperature_model': 'sapm', 'losses_model': 'no_loss'}
     system.module_parameters.pop('A0')  # remove a parameter
     with pytest.raises(ValueError):
         ModelChain(system, location, **kwargs)
@@ -440,13 +514,13 @@ def test_invalid_dc_model_params(system, cec_dc_snl_ac_system,
 
 
 @pytest.mark.parametrize('model', [
-    'dc_model', 'ac_model', 'aoi_model', 'spectral_model', 'losses_model',
-    'temp_model', 'losses_model'
+    'dc_model', 'ac_model', 'aoi_model', 'spectral_model',
+    'temperature_model', 'losses_model'
 ])
 def test_invalid_models(model, system, location):
     kwargs = {'dc_model': 'pvwatts', 'ac_model': 'pvwatts',
               'aoi_model': 'no_loss', 'spectral_model': 'no_loss',
-              'temp_model': 'sapm', 'losses_model': 'no_loss'}
+              'temperature_model': 'sapm', 'losses_model': 'no_loss'}
     kwargs[model] = 'invalid'
     with pytest.raises(ValueError):
         ModelChain(system, location, **kwargs)
@@ -464,11 +538,47 @@ def test_deprecated_07():
     # does not matter what the parameters are, just fake it until we make it
     module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1,
                          'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1}
-    system = PVSystem(module_parameters=module_parameters)
+    temp_model_params = {'a': -3.5, 'b': -0.05, 'deltaT': 3}
+    system = PVSystem(module_parameters=module_parameters,
+                      temperature_model_parameters=temp_model_params)
     with pytest.warns(pvlibDeprecationWarning):
         ModelChain(system, location,
                    dc_model='singlediode',  # this should fail after 0.7
                    aoi_model='no_loss', spectral_model='no_loss',
+                   temperature_model='sapm',
+                   ac_model='snlinverter')
+
+
+@fail_on_pvlib_version('0.8')
+def test_deprecated_08():
+    # explicit system creation call because fail_on_pvlib_version
+    # does not support decorators.
+    # does not matter what the parameters are, just fake it until we make it
+    module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1,
+                         'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1}
+    # do not assign PVSystem.temperature_model_parameters
+    system = PVSystem(module_parameters=module_parameters)
+    with pytest.warns(pvlibDeprecationWarning):
+        ModelChain(system, location,
+                   dc_model='desoto',
+                   aoi_model='no_loss', spectral_model='no_loss',
+                   temp_model='sapm',
+                   ac_model='snlinverter')
+    system = PVSystem(module_parameters=module_parameters)
+    with pytest.warns(pvlibDeprecationWarning):
+        ModelChain(system, location,
+                   dc_model='desoto',
+                   aoi_model='no_loss', spectral_model='no_loss',
+                   temperature_model='sapm',
+                   temp_model='sapm',
+                   ac_model='snlinverter')
+    system = PVSystem(module_parameters=module_parameters)
+    with pytest.raises(ValueError):
+        ModelChain(system, location,
+                   dc_model='desoto',
+                   aoi_model='no_loss', spectral_model='no_loss',
+                   temperature_model='pvsyst',
+                   temp_model='sapm',
                    ac_model='snlinverter')
 
 
@@ -477,7 +587,9 @@ def test_deprecated_07():
 def test_deprecated_clearsky_07():
     # explicit system creation call because fail_on_pvlib_version
     # does not support decorators.
-    system = PVSystem(module_parameters={'pdc0': 1, 'gamma_pdc': -0.003})
+    system = PVSystem(module_parameters={'pdc0': 1, 'gamma_pdc': -0.003},
+                      temperature_model_parameters={'a': -3.5, 'b': -0.05,
+                                                    'deltaT': 3})
     location = Location(32.2, -110.9)
     mc = ModelChain(system, location, dc_model='pvwatts', ac_model='pvwatts',
                     aoi_model='no_loss', spectral_model='no_loss')
@@ -488,7 +600,8 @@ def test_deprecated_clearsky_07():
 
 
 @requires_scipy
-def test_basic_chain_required(sam_data, cec_inverter_parameters):
+def test_basic_chain_required(sam_data, cec_inverter_parameters,
+                              sapm_temperature_cs5p_220m):
     times = pd.date_range(start='20160101 1200-0700',
                           end='20160101 1800-0700', freq='6H')
     latitude = 32
@@ -496,15 +609,17 @@ def test_basic_chain_required(sam_data, cec_inverter_parameters):
     altitude = 700
     modules = sam_data['sandiamod']
     module_parameters = modules['Canadian_Solar_CS5P_220M___2009_']
+    temp_model_params = sapm_temperature_cs5p_220m.copy()
     with pytest.raises(ValueError):
         dc, ac = modelchain.basic_chain(
-            times, latitude, longitude, module_parameters,
+            times, latitude, longitude, module_parameters, temp_model_params,
             cec_inverter_parameters, altitude=altitude
         )
 
 
 @requires_scipy
-def test_basic_chain_alt_az(sam_data, cec_inverter_parameters):
+def test_basic_chain_alt_az(sam_data, cec_inverter_parameters,
+                            sapm_temperature_cs5p_220m):
     times = pd.date_range(start='20160101 1200-0700',
                           end='20160101 1800-0700', freq='6H')
     latitude = 32.2
@@ -513,9 +628,10 @@ def test_basic_chain_alt_az(sam_data, cec_inverter_parameters):
     surface_azimuth = 0
     modules = sam_data['sandiamod']
     module_parameters = modules['Canadian_Solar_CS5P_220M___2009_']
-
+    temp_model_params = sapm_temperature_cs5p_220m.copy()
     dc, ac = modelchain.basic_chain(times, latitude, longitude,
-                                    module_parameters, cec_inverter_parameters,
+                                    module_parameters,  temp_model_params,
+                                    cec_inverter_parameters,
                                     surface_tilt=surface_tilt,
                                     surface_azimuth=surface_azimuth)
 
@@ -525,7 +641,8 @@ def test_basic_chain_alt_az(sam_data, cec_inverter_parameters):
 
 
 @requires_scipy
-def test_basic_chain_strategy(sam_data, cec_inverter_parameters):
+def test_basic_chain_strategy(sam_data, cec_inverter_parameters,
+                              sapm_temperature_cs5p_220m):
     times = pd.date_range(start='20160101 1200-0700',
                           end='20160101 1800-0700', freq='6H')
     latitude = 32.2
@@ -533,10 +650,11 @@ def test_basic_chain_strategy(sam_data, cec_inverter_parameters):
     altitude = 700
     modules = sam_data['sandiamod']
     module_parameters = modules['Canadian_Solar_CS5P_220M___2009_']
-
+    temp_model_params = sapm_temperature_cs5p_220m.copy()
     dc, ac = modelchain.basic_chain(
-        times, latitude, longitude, module_parameters, cec_inverter_parameters,
-        orientation_strategy='south_at_latitude_tilt', altitude=altitude)
+        times, latitude, longitude, module_parameters, temp_model_params,
+        cec_inverter_parameters, orientation_strategy='south_at_latitude_tilt',
+        altitude=altitude)
 
     expected = pd.Series(np.array([  183.522449305,  -2.00000000e-02]),
                          index=times)
@@ -544,7 +662,8 @@ def test_basic_chain_strategy(sam_data, cec_inverter_parameters):
 
 
 @requires_scipy
-def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters):
+def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters,
+                                       sapm_temperature_cs5p_220m):
     times = pd.date_range(start='20160101 1200-0700',
                           end='20160101 1800-0700', freq='6H')
     latitude = 32.2
@@ -554,9 +673,10 @@ def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters):
     surface_azimuth = 0
     modules = sam_data['sandiamod']
     module_parameters = modules['Canadian_Solar_CS5P_220M___2009_']
-
+    temp_model_params = sapm_temperature_cs5p_220m.copy()
     dc, ac = modelchain.basic_chain(times, latitude, longitude,
-                                    module_parameters, cec_inverter_parameters,
+                                    module_parameters, temp_model_params,
+                                    cec_inverter_parameters,
                                     surface_tilt=surface_tilt,
                                     surface_azimuth=surface_azimuth,
                                     pressure=93194)
@@ -566,7 +686,8 @@ def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters):
     assert_series_equal(ac, expected, check_less_precise=1)
 
     dc, ac = modelchain.basic_chain(times, latitude, longitude,
-                                    module_parameters, cec_inverter_parameters,
+                                    module_parameters, temp_model_params,
+                                    cec_inverter_parameters,
                                     surface_tilt=surface_tilt,
                                     surface_azimuth=surface_azimuth,
                                     altitude=altitude)
@@ -596,7 +717,7 @@ def test_ModelChain___repr__(system, location, strategy, strategy_str):
         '  ac_model: snlinverter',
         '  aoi_model: sapm_aoi_loss',
         '  spectral_model: sapm_spectral_loss',
-        '  temp_model: sapm_temp',
+        '  temperature_model: sapm_temp',
         '  losses_model: no_extra_losses'
     ])
 
diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py
index ce854be..41589c2 100644
--- a/pvlib/test/test_pvsystem.py
+++ b/pvlib/test/test_pvsystem.py
@@ -17,8 +17,10 @@ from pvlib import irradiance
 from pvlib import atmosphere
 from pvlib import solarposition
 from pvlib.location import Location
+from pvlib import temperature
+from pvlib._deprecation import pvlibDeprecationWarning
 
-from conftest import needs_numpy_1_10, requires_scipy
+from conftest import needs_numpy_1_10, requires_scipy, fail_on_pvlib_version
 
 
 def test_systemdef_tmy3():
@@ -497,6 +499,94 @@ def test_PVSystem_sapm_effective_irradiance(sapm_module_params, mocker):
     assert_allclose(out, 1, atol=0.1)
 
 
+def test_PVSystem_sapm_celltemp(mocker):
+    a, b, deltaT = (-3.47, -0.0594, 3)  # open_rack_glass_glass
+    temp_model_params = {'a': a, 'b': b, 'deltaT': deltaT}
+    system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params)
+    mocker.spy(temperature, 'sapm_cell')
+    temps = 25
+    irrads = 1000
+    winds = 1
+    out = system.sapm_celltemp(irrads, temps, winds)
+    temperature.sapm_cell.assert_called_once_with(irrads, temps, winds, a, b,
+                                                  deltaT)
+    assert_allclose(out, 57, atol=1)
+
+
+def test_PVSystem_sapm_celltemp_kwargs(mocker):
+    temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][
+        'open_rack_glass_glass']
+    system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params)
+    mocker.spy(temperature, 'sapm_cell')
+    temps = 25
+    irrads = 1000
+    winds = 1
+    out = system.sapm_celltemp(irrads, temps, winds)
+    temperature.sapm_cell.assert_called_once_with(irrads, temps, winds,
+                                                  temp_model_params['a'],
+                                                  temp_model_params['b'],
+                                                  temp_model_params['deltaT'])
+    assert_allclose(out, 57, atol=1)
+
+
+def test_PVSystem_pvsyst_celltemp(mocker):
+    parameter_set = 'insulated'
+    temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['pvsyst'][
+        parameter_set]
+    alpha_absorption = 0.85
+    eta_m = 0.17
+    module_parameters = {'alpha_absorption': alpha_absorption, 'eta_m': eta_m}
+    system = pvsystem.PVSystem(module_parameters=module_parameters,
+                               temperature_model_parameters=temp_model_params)
+    mocker.spy(temperature, 'pvsyst_cell')
+    irrad = 800
+    temp = 45
+    wind = 0.5
+    out = system.pvsyst_celltemp(irrad, temp, wind_speed=wind)
+    temperature.pvsyst_cell.assert_called_once_with(
+        irrad, temp, wind, temp_model_params['u_c'], temp_model_params['u_v'],
+        eta_m, alpha_absorption)
+    assert (out < 90) and (out > 70)
+
+
+def test_PVSystem_pvsyst_celltemp_kwargs(mocker):
+    temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['pvsyst'][
+        'insulated']
+    alpha_absorption = 0.85
+    eta_m = 0.17
+    module_parameters = {'alpha_absorption': alpha_absorption, 'eta_m': eta_m}
+    system = pvsystem.PVSystem(module_parameters=module_parameters,
+                               temperature_model_parameters=temp_model_params)
+    mocker.spy(temperature, 'pvsyst_cell')
+    irrad = 800
+    temp = 45
+    wind = 0.5
+    out = system.pvsyst_celltemp(irrad, temp, wind_speed=wind)
+    temperature.pvsyst_cell.assert_called_once_with(
+        irrad, temp, wind, temp_model_params['u_c'], temp_model_params['u_v'],
+        eta_m, alpha_absorption)
+    assert (out < 90) and (out > 70)
+
+
+def test__infer_temperature_model_params():
+    system = pvsystem.PVSystem(module_parameters={},
+                               racking_model='open_rack',
+                               module_type='glass_polymer')
+    expected = temperature.TEMPERATURE_MODEL_PARAMETERS[
+        'sapm']['open_rack_glass_polymer']
+    assert expected == system._infer_temperature_model_params()
+    expected = temperature.TEMPERATURE_MODEL_PARAMETERS[
+        'pvsyst']['freestanding']
+    system = pvsystem.PVSystem(module_parameters={},
+                               racking_model='freestanding',
+                               module_type='glass_polymer')
+    assert expected == system._infer_temperature_model_params()
+    system = pvsystem.PVSystem(module_parameters={},
+                               racking_model='not_a_rack_model',
+                               module_type='glass_polymer')
+    assert {} == system._infer_temperature_model_params()
+
+
 def test_calcparams_desoto(cec_module_params):
     times = pd.date_range(start='2015-01-01', periods=3, freq='12H')
     effective_irradiance = pd.Series([0.0, 800.0, 800.0], index=times)
@@ -1147,114 +1237,6 @@ def test_PVSystem_scale_voltage_current_power(mocker):
     m.assert_called_once_with(data, voltage=2, current=3)
 
 
-def test_sapm_celltemp():
-    default = pvsystem.sapm_celltemp(900, 5, 20)
-    assert_allclose(default['temp_cell'], 43.509, 3)
-    assert_allclose(default['temp_module'], 40.809, 3)
-    assert_frame_equal(default, pvsystem.sapm_celltemp(900, 5, 20,
-                                                       [-3.47, -.0594, 3]))
-
-
-def test_sapm_celltemp_dict_like():
-    default = pvsystem.sapm_celltemp(900, 5, 20)
-    assert_allclose(default['temp_cell'], 43.509, 3)
-    assert_allclose(default['temp_module'], 40.809, 3)
-    model = {'a': -3.47, 'b': -.0594, 'deltaT': 3}
-    assert_frame_equal(default, pvsystem.sapm_celltemp(900, 5, 20, model))
-    model = pd.Series(model)
-    assert_frame_equal(default, pvsystem.sapm_celltemp(900, 5, 20, model))
-
-
-def test_sapm_celltemp_with_index():
-    times = pd.date_range(start='2015-01-01', end='2015-01-02', freq='12H')
-    temps = pd.Series([0, 10, 5], index=times)
-    irrads = pd.Series([0, 500, 0], index=times)
-    winds = pd.Series([10, 5, 0], index=times)
-
-    pvtemps = pvsystem.sapm_celltemp(irrads, winds, temps)
-
-    expected = pd.DataFrame({'temp_cell':[0., 23.06066166, 5.],
-                             'temp_module':[0., 21.56066166, 5.]},
-                            index=times)
-
-    assert_frame_equal(expected, pvtemps)
-
-
-def test_PVSystem_sapm_celltemp(mocker):
-    racking_model = 'roof_mount_cell_glassback'
-
-    system = pvsystem.PVSystem(racking_model=racking_model)
-    mocker.spy(pvsystem, 'sapm_celltemp')
-    temps = 25
-    irrads = 1000
-    winds = 1
-    out = system.sapm_celltemp(irrads, winds, temps)
-    pvsystem.sapm_celltemp.assert_called_once_with(
-        irrads, winds, temps, model=racking_model)
-    assert isinstance(out, pd.DataFrame)
-    assert out.shape == (1, 2)
-
-
-def test_pvsyst_celltemp_default():
-    default = pvsystem.pvsyst_celltemp(900, 20, 5)
-    assert_allclose(default, 45.137, 0.001)
-
-
-def test_pvsyst_celltemp_non_model():
-    tup_non_model = pvsystem.pvsyst_celltemp(900, 20, 5, 0.1,
-                                             model_params=(23.5, 6.25))
-    assert_allclose(tup_non_model, 33.315, 0.001)
-
-    list_non_model = pvsystem.pvsyst_celltemp(900, 20, 5, 0.1,
-                                              model_params=[26.5, 7.68])
-    assert_allclose(list_non_model, 31.233, 0.001)
-
-
-def test_pvsyst_celltemp_model_wrong_type():
-    with pytest.raises(TypeError):
-        pvsystem.pvsyst_celltemp(
-            900, 20, 5, 0.1,
-            model_params={"won't": 23.5, "work": 7.68})
-
-
-def test_pvsyst_celltemp_model_non_option():
-    with pytest.raises(KeyError):
-        pvsystem.pvsyst_celltemp(
-            900, 20, 5, 0.1,
-            model_params="not_an_option")
-
-
-def test_pvsyst_celltemp_with_index():
-    times = pd.date_range(start="2015-01-01", end="2015-01-02", freq="12H")
-    temps = pd.Series([0, 10, 5], index=times)
-    irrads = pd.Series([0, 500, 0], index=times)
-    winds = pd.Series([10, 5, 0], index=times)
-
-    pvtemps = pvsystem.pvsyst_celltemp(irrads, temps, wind_speed=winds)
-    expected = pd.Series([0.0, 23.96551, 5.0], index=times)
-    assert_series_equal(expected, pvtemps)
-
-
-def test_PVSystem_pvsyst_celltemp(mocker):
-    racking_model = 'insulated'
-    alpha_absorption = 0.85
-    eta_m = 0.17
-    module_parameters = {}
-    module_parameters['alpha_absorption'] = alpha_absorption
-    module_parameters['eta_m'] = eta_m
-    system = pvsystem.PVSystem(racking_model=racking_model,
-                               module_parameters=module_parameters)
-    mocker.spy(pvsystem, 'pvsyst_celltemp')
-    irrad = 800
-    temp = 45
-    wind = 0.5
-    out = system.pvsyst_celltemp(irrad, temp, wind_speed=wind)
-    pvsystem.pvsyst_celltemp.assert_called_once_with(
-        irrad, temp, wind, eta_m, alpha_absorption, racking_model)
-    assert isinstance(out, float)
-    assert out < 90 and out > 70
-
-
 def test_adrinverter(sam_data):
     inverters = sam_data['adrinverter']
     testinv = 'Ablerex_Electronics_Co___Ltd___' \
@@ -1425,7 +1407,9 @@ def test_PVSystem_localize_with_latlon():
 def test_PVSystem___repr__():
     system = pvsystem.PVSystem(module='blah', inverter='blarg', name='pv ftw')
 
-    expected = 'PVSystem: \n  name: pv ftw\n  surface_tilt: 0\n  surface_azimuth: 180\n  module: blah\n  inverter: blarg\n  albedo: 0.25\n  racking_model: open_rack_cell_glassback'
+    expected = ('PVSystem: \n  name: pv ftw\n  surface_tilt: 0\n  '
+                'surface_azimuth: 180\n  module: blah\n  inverter: blarg\n  '
+                'albedo: 0.25\n  racking_model: open_rack')
 
     assert system.__repr__() == expected
 
@@ -1434,7 +1418,10 @@ def test_PVSystem_localize___repr__():
     system = pvsystem.PVSystem(module='blah', inverter='blarg', name='pv ftw')
     localized_system = system.localize(latitude=32, longitude=-111)
 
-    expected = 'LocalizedPVSystem: \n  name: None\n  latitude: 32\n  longitude: -111\n  altitude: 0\n  tz: UTC\n  surface_tilt: 0\n  surface_azimuth: 180\n  module: blah\n  inverter: blarg\n  albedo: 0.25\n  racking_model: open_rack_cell_glassback'
+    expected = ('LocalizedPVSystem: \n  name: None\n  latitude: 32\n  '
+                'longitude: -111\n  altitude: 0\n  tz: UTC\n  '
+                'surface_tilt: 0\n  surface_azimuth: 180\n  module: blah\n  '
+                'inverter: blarg\n  albedo: 0.25\n  racking_model: open_rack')
 
     assert localized_system.__repr__() == expected
 
@@ -1463,7 +1450,10 @@ def test_LocalizedPVSystem___repr__():
                                                   inverter='blarg',
                                                   name='my name')
 
-    expected = 'LocalizedPVSystem: \n  name: my name\n  latitude: 32\n  longitude: -111\n  altitude: 0\n  tz: UTC\n  surface_tilt: 0\n  surface_azimuth: 180\n  module: blah\n  inverter: blarg\n  albedo: 0.25\n  racking_model: open_rack_cell_glassback'
+    expected = ('LocalizedPVSystem: \n  name: my name\n  latitude: 32\n  '
+                'longitude: -111\n  altitude: 0\n  tz: UTC\n  '
+                'surface_tilt: 0\n  surface_azimuth: 180\n  module: blah\n  '
+                'inverter: blarg\n  albedo: 0.25\n  racking_model: open_rack')
 
     assert localized_system.__repr__() == expected
 
@@ -1622,3 +1612,60 @@ def test_PVSystem_pvwatts_ac_kwargs(mocker):
     pvsystem.pvwatts_ac.assert_called_once_with(pdc,
                                                 **system.inverter_parameters)
     assert out < pdc
+
+
+@fail_on_pvlib_version('0.8')
+def test_deprecated_08():
+    with pytest.warns(pvlibDeprecationWarning):
+        pvsystem.sapm_celltemp(1000, 25, 1)
+    with pytest.warns(pvlibDeprecationWarning):
+        pvsystem.pvsyst_celltemp(1000, 25)
+    module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1,
+                         'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1}
+    with pytest.warns(pvlibDeprecationWarning):
+        pvsystem.PVSystem(module_parameters=module_parameters,
+                          racking_model='open', module_type='glass_glass')
+
+
+@fail_on_pvlib_version('0.8')
+def test__pvsyst_celltemp_translator():
+    result = pvsystem._pvsyst_celltemp_translator(900, 20, 5)
+    assert_allclose(result, 45.137, 0.001)
+    result = pvsystem._pvsyst_celltemp_translator(900, 20, 5, 0.1, 0.9,
+                                                  [29.0, 0.0])
+    assert_allclose(result, 45.137, 0.001)
+    result = pvsystem._pvsyst_celltemp_translator(poa_global=900, temp_air=20,
+                                                  wind_speed=5)
+    assert_allclose(result, 45.137, 0.001)
+    result = pvsystem._pvsyst_celltemp_translator(900, 20, wind_speed=5)
+    assert_allclose(result, 45.137, 0.001)
+    result = pvsystem._pvsyst_celltemp_translator(900, 20, wind_speed=5.0,
+                                                  u_c=23.5, u_v=6.25,
+                                                  eta_m=0.1)
+    assert_allclose(result, 33.315, 0.001)
+    result = pvsystem._pvsyst_celltemp_translator(900, 20, wind_speed=5.0,
+                                                  eta_m=0.1,
+                                                  model_params=[23.5, 6.25])
+    assert_allclose(result, 33.315, 0.001)
+    result = pvsystem._pvsyst_celltemp_translator(900, 20, wind_speed=5.0,
+                                                  eta_m=0.1,
+                                                  model_params=(23.5, 6.25))
+    assert_allclose(result, 33.315, 0.001)
+
+
+@fail_on_pvlib_version('0.8')
+def test__sapm_celltemp_translator():
+    result = pvsystem._sapm_celltemp_translator(900, 5, 20,
+                                                'open_rack_glass_glass')
+    assert_allclose(result, 43.509, 3)
+    result = pvsystem._sapm_celltemp_translator(900, 5, temp_air=20,
+                                                model='open_rack_glass_glass')
+    assert_allclose(result, 43.509, 3)
+    params = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][
+        'open_rack_glass_glass']
+    result = pvsystem._sapm_celltemp_translator(900, 5, 20, params)
+    assert_allclose(result, 43.509, 3)
+    result = pvsystem._sapm_celltemp_translator(900, 5, 20,
+                                                [params['a'], params['b'],
+                                                 params['deltaT']])
+    assert_allclose(result, 43.509, 3)
diff --git a/pvlib/test/test_temperature.py b/pvlib/test/test_temperature.py
new file mode 100644
index 0000000..c3112d8
--- /dev/null
+++ b/pvlib/test/test_temperature.py
@@ -0,0 +1,99 @@
+import pandas as pd
+import numpy as np
+
+import pytest
+from pandas.util.testing import assert_series_equal
+from numpy.testing import assert_allclose
+
+from pvlib import temperature
+
+
+@pytest.fixture
+def sapm_default():
+    return temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][
+        'open_rack_glass_glass']
+
+
+def test_sapm_cell(sapm_default):
+    default = temperature.sapm_cell(900, 20, 5, sapm_default['a'],
+                                    sapm_default['b'], sapm_default['deltaT'])
+    assert_allclose(default, 43.509, 3)
+
+
+def test_sapm_module(sapm_default):
+    default = temperature.sapm_module(900, 20, 5, sapm_default['a'],
+                                      sapm_default['b'])
+    assert_allclose(default, 40.809, 3)
+
+
+def test_sapm_ndarray(sapm_default):
+    temps = np.array([0, 10, 5])
+    irrads = np.array([0, 500, 0])
+    winds = np.array([10, 5, 0])
+    cell_temps = temperature.sapm_cell(irrads, temps, winds, sapm_default['a'],
+                                       sapm_default['b'],
+                                       sapm_default['deltaT'])
+    module_temps = temperature.sapm_module(irrads, temps, winds,
+                                           sapm_default['a'],
+                                           sapm_default['b'])
+    expected_cell = np.array([0., 23.06066166, 5.])
+    expected_module = np.array([0., 21.56066166, 5.])
+    assert_allclose(expected_cell, cell_temps, 3)
+    assert_allclose(expected_module, module_temps, 3)
+
+
+def test_sapm_series(sapm_default):
+    times = pd.date_range(start='2015-01-01', end='2015-01-02', freq='12H')
+    temps = pd.Series([0, 10, 5], index=times)
+    irrads = pd.Series([0, 500, 0], index=times)
+    winds = pd.Series([10, 5, 0], index=times)
+    cell_temps = temperature.sapm_cell(irrads, temps, winds, sapm_default['a'],
+                                       sapm_default['b'],
+                                       sapm_default['deltaT'])
+    module_temps = temperature.sapm_module(irrads, temps, winds,
+                                           sapm_default['a'],
+                                           sapm_default['b'])
+    expected_cell = pd.Series([0., 23.06066166, 5.], index=times)
+    expected_module = pd.Series([0., 21.56066166, 5.], index=times)
+    assert_series_equal(expected_cell, cell_temps)
+    assert_series_equal(expected_module, module_temps)
+
+
+def test_pvsyst_cell_default():
+    result = temperature.pvsyst_cell(900, 20, 5)
+    assert_allclose(result, 45.137, 0.001)
+
+
+def test_pvsyst_cell_kwargs():
+    result = temperature.pvsyst_cell(900, 20, wind_speed=5.0, u_c=23.5,
+                                     u_v=6.25, eta_m=0.1)
+    assert_allclose(result, 33.315, 0.001)
+
+
+def test_pvsyst_cell_ndarray():
+    temps = np.array([0, 10, 5])
+    irrads = np.array([0, 500, 0])
+    winds = np.array([10, 5, 0])
+    result = temperature.pvsyst_cell(irrads, temps, wind_speed=winds)
+    expected = np.array([0.0, 23.96551, 5.0])
+    assert_allclose(expected, result, 3)
+
+
+def test_pvsyst_cell_series():
+    times = pd.date_range(start="2015-01-01", end="2015-01-02", freq="12H")
+    temps = pd.Series([0, 10, 5], index=times)
+    irrads = pd.Series([0, 500, 0], index=times)
+    winds = pd.Series([10, 5, 0], index=times)
+
+    result = temperature.pvsyst_cell(irrads, temps, wind_speed=winds)
+    expected = pd.Series([0.0, 23.96551, 5.0], index=times)
+    assert_series_equal(expected, result)
+
+
+def test__temperature_model_params():
+    params = temperature._temperature_model_params('sapm',
+                                                   'open_rack_glass_glass')
+    assert params == temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][
+        'open_rack_glass_glass']
+    with pytest.raises(KeyError):
+        temperature._temperature_model_params('sapm', 'not_a_parameter_set')
diff --git a/pvlib/test/test_tracking.py b/pvlib/test/test_tracking.py
index 8ab8032..983d8ae 100644
--- a/pvlib/test/test_tracking.py
+++ b/pvlib/test/test_tracking.py
@@ -440,7 +440,11 @@ def test_get_irradiance():
 def test_SingleAxisTracker___repr__():
     system = tracking.SingleAxisTracker(max_angle=45, gcr=.25,
                                         module='blah', inverter='blarg')
-    expected = 'SingleAxisTracker: \n  axis_tilt: 0\n  axis_azimuth: 0\n  max_angle: 45\n  backtrack: True\n  gcr: 0.25\n  name: None\n  surface_tilt: None\n  surface_azimuth: None\n  module: blah\n  inverter: blarg\n  albedo: 0.25\n  racking_model: open_rack_cell_glassback'
+    expected = ('SingleAxisTracker: \n  axis_tilt: 0\n  axis_azimuth: 0\n  '
+                'max_angle: 45\n  backtrack: True\n  gcr: 0.25\n  '
+                'name: None\n  surface_tilt: None\n  surface_azimuth: None\n  '
+                'module: blah\n  inverter: blarg\n  albedo: 0.25\n  '
+                'racking_model: open_rack')
     assert system.__repr__() == expected
 
 
@@ -451,6 +455,11 @@ def test_LocalizedSingleAxisTracker___repr__():
                                                            inverter='blarg',
                                                            gcr=0.25)
 
-    expected = 'LocalizedSingleAxisTracker: \n  axis_tilt: 0\n  axis_azimuth: 0\n  max_angle: 90\n  backtrack: True\n  gcr: 0.25\n  name: None\n  surface_tilt: None\n  surface_azimuth: None\n  module: blah\n  inverter: blarg\n  albedo: 0.25\n  racking_model: open_rack_cell_glassback\n  latitude: 32\n  longitude: -111\n  altitude: 0\n  tz: UTC'
+    expected = ('LocalizedSingleAxisTracker: \n  axis_tilt: 0\n  '
+                'axis_azimuth: 0\n  max_angle: 90\n  backtrack: True\n  '
+                'gcr: 0.25\n  name: None\n  surface_tilt: None\n  '
+                'surface_azimuth: None\n  module: blah\n  inverter: blarg\n  '
+                'albedo: 0.25\n  racking_model: open_rack\n  '
+                'latitude: 32\n  longitude: -111\n  altitude: 0\n  tz: UTC')
 
     assert localized_system.__repr__() == expected