first commit

This commit is contained in:
Senad Uka
2017-11-07 09:23:57 +01:00
commit 0eee92660a
356 changed files with 747259 additions and 0 deletions

4
.babelrc Normal file
View File

@@ -0,0 +1,4 @@
{
"presets": ["stage-2", "es2015" ],
"plugins": ["transform-object-assign"]
}

10
.cfignore Normal file
View File

@@ -0,0 +1,10 @@
env
__pycache__
Gemfile*
node_modules
test
spec
lib
package.json
npm-shrinkwrap.json
Rakefile

23
.editorconfig Normal file
View File

@@ -0,0 +1,23 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
# Matches the exact files either package.json or .travis.yml
[{package.json,*.js}]
indent_style = space
indent_size = 2

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules
env
.idea
__pycache__
.DS_Store
*.log
*cache
.js.map

80
Dockerfile Normal file
View File

@@ -0,0 +1,80 @@
from python:3.5
RUN apt-get update
RUN apt-get install -y \
git \
imagemagick \
libmagickcore-dev \
libxml2-dev \
libxslt1-dev \
ruby \
ruby-dev
RUN gem install bundler
RUN apt-get install -y \
build-essential \
chrpath \
libssl-dev \
libxft-dev \
libfreetype6 \
libfreetype6-dev \
libfontconfig1 \
libfontconfig1-dev \
wget \
curl
# postgres client
RUN apt-get install -y \
postgresql-client \
postgresql-client-common \
postgresql-contrib \
libpq-dev
# redis client
RUN apt-get install -y redis-tools
# phantom.js
ENV PHANTOM_JS=phantomjs-2.1.1-linux-x86_64
RUN wget https://bitbucket.org/ariya/phantomjs/downloads/$PHANTOM_JS.tar.bz2
RUN tar xvjf $PHANTOM_JS.tar.bz2 && \
mv $PHANTOM_JS /usr/local/share && \
ln -sf /usr/local/share/$PHANTOM_JS/bin/phantomjs /usr/local/bin
# nodejs
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash -
RUN apt-get install -y nodejs
# configure ssh
RUN apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN echo 'root:screencast' | chpasswd
RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config
# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
ENV NOTVISIBLE "in users profile"
RUN echo "export VISIBLE=now" >> /etc/profile
RUN sed -ie 's/Port 22/Port 2222/g' /etc/ssh/sshd_config
# install python libs
ADD ./requirements.txt /requirements/
ADD ./requirements.test.txt /requirements/
WORKDIR /requirements
RUN pip install -r requirements.txt
RUN pip install -r requirements.test.txt
RUN pip3 install invoke
# install nodejs libs
ADD ./package.json /code/
WORKDIR /code
RUN npm install
CMD ["/usr/sbin/sshd", "-D"]
#CMD ["invoke", "serve_debug"]

34
Dockerfile.db Normal file
View File

@@ -0,0 +1,34 @@
from ubuntu:16.04
RUN apt-get update && apt-get install -y sudo vim
RUN apt-get install -y \
apt-utils \
postgresql \
postgresql-client \
postgresql-client-common \
postgresql-contrib \
libpq-dev
RUN sudo cat /etc/postgresql/9.5/main/pg_hba.conf
RUN sudo echo "local all postgres trust" > /etc/postgresql/9.5/main/pg_hba.conf && \
sudo echo "local all all trust" >> /etc/postgresql/9.5/main/pg_hba.conf && \
sudo echo "host all all 127.0.0.1/32 trust" >> /etc/postgresql/9.5/main/pg_hba.conf && \
sudo echo "host all all ::1/128 trust" >> /etc/postgresql/9.5/main/pg_hba.conf
USER postgres
RUN /etc/init.d/postgresql start && \
createdb test && \
createdb pivotal && \
echo "CREATE ROLE pivotal WITH UNENCRYPTED PASSWORD 'password';" | psql -U postgres && \
echo "ALTER ROLE pivotal WITH LOGIN;" | psql -U postgres && \
echo "GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA public TO pivotal;" | psql -U postgres && \
echo "GRANT CREATE, CONNECT ON DATABASE test TO pivotal;" | psql -U postgres && \
echo "GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA public TO pivotal;" | psql -U postgres && \
echo "GRANT CREATE, CONNECT ON DATABASE pivotal TO pivotal;" | psql -U postgres
EXPOSE 5432
CMD ["/usr/lib/postgresql/9.5/bin/postgres", "-D", "/var/lib/postgresql/9.5/main", "-c", "config_file=/etc/postgresql/9.5/main/postgresql.conf"]

4
Gemfile Normal file
View File

@@ -0,0 +1,4 @@
source 'https://rubygems.org'
gem 'sass'
gem 'pivotal-tracker'

46
Gemfile.lock Normal file
View File

@@ -0,0 +1,46 @@
GEM
remote: https://rubygems.org/
specs:
builder (3.2.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
domain_name (0.5.20160615)
unf (>= 0.0.5, < 1.0.0)
http-cookie (1.0.2)
domain_name (~> 0.5)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_portile2 (2.1.0)
netrc (0.11.0)
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
nokogiri-happymapper (0.5.9)
nokogiri (~> 1.5)
pivotal-tracker (0.5.13)
builder
crack
nokogiri (>= 1.5.5)
nokogiri-happymapper (>= 0.5.4)
rest-client (>= 1.8.0)
pkg-config (1.1.7)
rest-client (2.0.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
safe_yaml (1.0.4)
sass (3.4.22)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
PLATFORMS
ruby
DEPENDENCIES
pivotal-tracker
sass
BUNDLED WITH
1.12.5

1
Procfile Normal file
View File

@@ -0,0 +1 @@
web: python helix/main.py

286
README.md Normal file
View File

@@ -0,0 +1,286 @@
## Helix Calculator
- [Staging](https://sp-helix-staging.herokuapp.com)
- [PreProd](https://sp-helix-preprod.herokuapp.com)
- [Production](https://helix-calculator.sunpower.com/)
### Setup
#### macOS systems
This assumes you already have [homebrew](https://brew.sh) installed as well as an [ssh key added to your github account](https://help.github.com/articles/generating-an-ssh-key/).
This also assumes your username is pivotal, see the [circleci configuration](circleci.yml) for examples of how to add a pivotal user to postgres.
```sh
xcode-select --install
brew install python3 node nodenv redis phantomjs imagemagick@6 postgres brew-services
brew services start postgres
createdb test
createdb pivotal
git clone git@github.com:SunPower/Helix_Roof_Calculator.git
cd Helix_Roof_Calculator
pyvenv env
source env/bin/activate
pip install invoke
invoke install
npm install
invoke db_migrate
```
This installs python3, the [invoke utility](https://github.com/pyinvoke/invoke), xcode, node, imagemagick, redis, and postgres. It also sets up a virtual environment for the calculator, installs all the python dependencies into the virtual environment, and installs all the node dependencies.
Note that invoke should be installed after the virtual env is installed to ensure that the python 3.x version of invoke is used (instead of the system version, which is typically 2.7.x).
#### Linux/Debian-based systems
While most of this README assumes you're running macOS, for linux/debian based systems (e.g. ubuntu), follow these instructions. See the mac section for an explanation.
##### PostgreSQL
Postgres needs to be installed and configured before we do anything else.
```
sudo apt-get install postgresql postgresql-client postgresql-client-common postgresql-contrib libpq-dev
# Modify /etc/postgresql/common/*/pg_hba.conf and set local connections to trust
sudo -u postgres createdb test
sudo -u postgres createdb pivotal
# create pivotal role in postgres, grant it access to the test/pivotal databases.
echo "CREATE ROLE pivotal WITH UNENCRYPTED PASSWORD 'password';" | psql -U postgres
echo "ALTER ROLE pivotal WITH LOGIN;" | psql -U postgres
echo "GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA test.public TO pivotal;" | psql -U postgres
echo "GRANT CREATE, CONNECT ON DATABASE test TO pivotal;" | psql -U postgres
echo "GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA pivotal.public TO pivotal;" | psql -U postgres
echo "GRANT CREATE, CONNECT ON DATABASE pivotal TO pivotal;" | psql -U postgres
```
This installs postgres and creates the database and roles that helix looks for in test/local usage. Because ubuntu is systemd, you can start/stop/restart the server by running something along the lines of `sudo systemctl [start|stop|restart] postgresql.service`. Please see [this ubuntu postgres documentation page](https://help.ubuntu.com/community/PostgreSQL) for more information.
The comment for the second line tells you how to enable connections to postgres without requiring a password. See http://dba.stackexchange.com/questions/83164/remove-password-requirement-for-user-postgres for more information
##### Other dependencies/the project
```sh
sudo apt-get install git python3 python3-dev python3-venv nodejs npm redis-server imagemagick libmagickcore-dev libxml2-dev libxslt1-dev ruby ruby-dev
sudo gem install bundler
python3 --version # make sure you have >3.5 installed!
git clone git@github.com:SunPower/Helix_Roof_Calculator.git
cd Helix_Roof_Calculator
pyvenv env
source env/bin/activate
pip install invoke
invoke install
npm install
invoke db_migrate
```
##### PhantomJS
```sh
sudo apt-get install build-essential chrpath libssl-dev libxft-dev libfreetype6 libfreetype6-dev libfontconfig1 libfontconfig1-dev
export PHANTOM_JS=phantomjs-2.1.1-linux-x86_64
wget https://bitbucket.org/ariya/phantomjs/downloads/$PHANTOM_JS.tar.bz2
sudo targ xvjf $PHANTOM_JS.tar.bz2
sudo mv $PHANTOM_JS /usr/local/share
sudo ln -sf /usr/local/share/$PHANTOM_JS/bin/phantomjs /usr/local/bin
```
Note that it doesn't work to use the nodejs-provided phantom, nor does apt-get install phantomjs work. Yes, it's annoying.
### Adding Dependencies
Because of how we're separating production dependencies and test dependencies, it's recommended that each new dependency be manually added to either the [requirements.txt](requirements.txt) or the [requirements.test.txt](requirements.test.txt), depending on whether it's a production or test dependency, respectively.
### Adding database migrations
To manage the database, we use [SQLAlchemy](http://www.sqlalchemy.org/).
To manage database migrations, we use [Alembic](http://alembic.zzzcomputing.com/en/latest/).
First, make your changes to the sql models (in the models/sql) package to reflect your new vision of the sql schema. Next, we need to take those changes and generate an alembic migration from them. Run `PYTHONPATH=. alembic revision -m "(migration description)"` to generate that migration. Now, add the changes (and revert instructions!) to the generated migration file. Finally, run `invoke db_migrate` to run that migration.
##### Downgrading a migration
There currently is not an invoke task to revert migrations. Instead, just directly use the alembic cli to do this, something along the lines of `PYTHONPATH=.:$PYTHONPATH alembic downgrade -1`. Setting PYTHONPATH to include the current directory will ensure that the correct sql models get picked up when alembic does the migration. To redo a migration using alembic, run `PYTHONPATH=.:$PYTHONPATH alembic upgrade +1`.
Please refer to the [Alembic Documentation](http://alembic.zzzcomputing.com/en/latest/) for more information on using Alembic.
### Running Tests
First, install PhantomJS (`brew install phantomjs`) (this should already be done as part of the setup).
Run `invoke test` from the project directory will run every test.
Note that we use [Python's built-in unittest library](https://docs.python.org/3/library/unittest.html) for our python tests. Additionally, there are a few other helper libraries we use in tests. See the [requirements.test.txt](requirements.test.txt) file for those.
##### Running Javascript Tests
`invoke test` will, by default, run both the python tests and the javascript tests. Because the python tests can take a while, it's advantageous to be able to run the javascript tests (used for the array visualization) separately, especially when working entirely on the visualization frontend.
To do this, run `invoke test_js` from your command. This spins up an instance of [karma](https://karma-runner.github.io/0.13/index.html) attached to (by default) Chrome which watches the [spec](spec) and the [helix/javascript](helix/javascript) directories for changes, and (usually) re-runs the javascript tests if anything changes.
Note that we use [Jasmine](http://jasmine.github.io/2.4/introduction.html) for our javascript specs.
### Building JS
Part of the application (as of now only files in `helix/javascript/array_summary`) are built using Webpack and leveraging ES6.
To run the build use this command from the root of the repository
```bash
./node_modules/.bin/webpack
```
### Running Locally
First, install redis (`brew install redis`), and then (in a separate terminal) run `redis-server /usr/local/etc/redis.conf` to run redis locally.
Next, install postgres (`brew install postgres`), and then start it (`brew services start postgres`), follow the instructions at the top for additional configuration information.
Then, tell alembic to migrate your postgres db to the latest (`invoke db_migrate`).
Run `invoke serve` from the project directory to run locally.
This will run the project on [port 5000](http://localhost:5000/).
##### Running in debug mode
Run `invoke serve_debug` to start the project in debug mode.
##### The image tests
Note that you may see image comparison tests failing for erroneous reasons. These are flaky and machine-dependent. If you've done anything to the image generation code, please verify visually that you're getting the output you want.
### Circle CI
[CI (Continuous Integration)](https://en.wikipedia.org/wiki/Continuous_integration) is a service we use that automatically runs all the tests whenever a new commit is pushed. This is important because it runs the code in a context-free (the container the code runs in is set up for each run, then destroyed afterwards) environment. We're using [Circle CI](http://circleci.com/) here because Rachel recommended it. In addition to automatically running tests, circle also automatically deploys successful builds to cloud foundry for us.
### Deploying to Heroku (locally)
Install the Heroku CLI by running `brew install heroku` from your terminal.
Add heroku git endpoints for the different environments you want to push to. Typically, you'll just want to push to staging (especially seeing that preprod/production are automatically pushed to heroku for you). The heroku git endpoints are:
- staging: `https://git.heroku.com/sp-helix-staging.git`
- preprod: `https://git.heroku.com/sp-helix-preprod.git`
- production: `https://git.heroku.com/sp-helix-production.git`
login to heroku with `heroku login`
Finally, push to heroku using git! (Something like `git push heroku master`)
Note that circle ci does all that for us!
#### Deploying to preprod
To deploy to preprod, rebase master (up to the commit you want) onto preprod, then push preprod to github. CI will automatically deploy to preprod assuming that tests pass.
To push the current state of master to preprod:
```bash
git pull -r # Pull from origin (assuming origin is github)
git checkout preprod # switch to preprod branch
git rebase master # rebase head of master onto preprod
git push origin preprod # push preprod to github
```
To push a past commit to preprod:
```bash
git pull -r # Pull from origin (assuming origin is github)
git log
# [grab the sha of the commit you want]
git checkout preprod # switch to preprod branch
git rebase f00d # replace f00d with the sha of the commit you want
git push origin preprod # push preprod to github
```
#### Deploying to production
To deploy to production, tag a commit as `release-<number>` (for example, `release-2`) and push the commit (`git push origin release-2`). CI will automatically deploy to production assuming that tests pass.
Please increment the release count each time.
### Setting up PyCharm
[PyCharm](https://www.jetbrains.com/pycharm/) is an IDE from Jetbrains for python. It can be installed by running `brew cask install pycharm`.
Open pycharm, input a license, and select to install the command line tool. Then, open the project in pycharm by running `charm .` from the project directory.
Right now, you can start to use pycharm as an editor, but it's not very useful as an IDE, there's a bit more configuration stuff before it's useful as an IDE.
#### Use the correct Python
By default, PyCharm assumes you're using Python 2.7.x, we need to tell it to not only use 3.5.x, but use the 3.5.x for our local virtual environment.
To do this, open the preferences window, and select `Project: Helix_Roof_Calculator` (our project directory is named helix instead of Helix_Roof_Calculator, so the screenshots will be different than what you see), then select Project Intepreter and change that to `3.5.2 virtualenv at [projectdirectory]/env`.
![Screenshot describing this](documentation/setup_pycharm_correct_python.png)
#### Configuring targets
For this, we're going to reference the target configuration menu on pycharm (this is the top-right triangle-esque menu)
![See here](documentation/setup_pycharm_target_configuration.png)
##### Running Tests
Make sure to change the python tests configuration for nosetests and unittests (target selection menu (top-right) -> Edit Configurations -> Python Tests -> Nosetests) so that the working directory is the project directory (not the test directory!).
![See this screenshot for unittests, nosestest looks similar](documentation/setup_pycharm_unittests_configuration.png)
Additionally, you can set up an 'All Tests' target, just click the + button, name the target something like 'All Tests', and tell it to run all in folder, and point it at the test subdirectory of the project. Your configuration should look something like the attached screenshot
![Configuring an All Tests target](documentation/setup_pycharm_alltests_target.png)
##### Running local server in PyCharm
Again, we need to edit the target configurations. This time add (or change, if it already exists) a python target named `helix`. It should be configured to run the `helix/main.py` script (on pivotal's machine, that field is filled with `/Users/pivotal/workspace/helix/helix/main.py`), with a working directory of the project directory (on pivotal's machine, that would be `/Users/pivotal/workspace/helix`). Additionally, the `FLASK_DEBUG` environment variable should be set to `1`. ![See this screenshot](documentation/setup_pycharm_server_target.png)
#### PhantomJS w/ PyCharm
You may need to ensure that phantomjs is in your PyCharm PATH. One way to do this is to symlink it into the project's env/bin folder
(for example `ln -s /usr/local/bin/phantomjs env/bin/`).
### Using Git
See [Github's documentation](https://help.github.com/). We prefer to commit directly to master, with small-ish commits that always have passing tests.
### Environment set-up using Docker
In order to set up complete environment using `docker-compose` you need to execute following line from project root directory:
```
docker-compose up -d
```
On Windows remember that repository should be inside Users dir. Additionally you need to set envvar:
export COMPOSE_CONVERT_WINDOWS_PATHS=1
Dependencies (including `node.js` packages) are resolved when building an image.
Environment will be exposed via ssh and accessible via following command:
```
ssh -p 2222 root@localhost
```
ssh password is: `screencast`
It can be seamlessly used via PyCharm by setting proper python interpreter:
`File -> Settings... -> Project: Helix_Roof_Calculator -> Project Interpreter`
![See this screenshot](documentation/pycharm_docker.png)
It's necessary to set path mappings when configuring targets:
![See this screenshot](documentation/pycharm_docker_config.png)
To initialize database, use following command:
```
ssh -p 2222 root@localhost 'cd /code && invoke db_migrate'
```
or:
```
docker exec -it helixroofcalculator_helix_1 invoke db_migrate
```
Debugging is possible directly from PyCharm, all code changes are transparent between host and container.

23
Rakefile Normal file
View File

@@ -0,0 +1,23 @@
namespace :ci do
desc 'Delivers stories to tracker'
task :deliver do
require 'pivotal-tracker'
TRACKER_TOKEN = 'a092a63a88a7e3f1cdd08ffca82ba53c'
TRACKER_PROJECT_ID = '1544689'
PivotalTracker::Client.token = TRACKER_TOKEN
PivotalTracker::Client.use_ssl = true
unpakt_project = PivotalTracker::Project.find(TRACKER_PROJECT_ID)
stories = unpakt_project.stories.all(:state => "finished", :story_type => ['bug', 'feature'])
stories.each do | story |
puts "Searching for #{story.id} in local git repo."
search_result = `git log -i --grep "[Finish(es)?|Fix(es)?] ##{story.id}"`
if search_result.length > 0
story.notes.create(:text => "Delivered by staging deploy script.")
story.update({"current_state" => "delivered"})
end
end
end
end

76
alembic.ini Normal file
View File

@@ -0,0 +1,76 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = db
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; this defaults
# to db/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat db/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
;sqlalchemy.url = postgres://pivotal:@localhost/pivotal
databases = local, test
[local]
sqlalchemy.url = postgres://pivotal:@localhost/pivotal
[test]
sqlalchemy.url = postgres://pivotal:@localhost/test
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

58
circle.yml Normal file
View File

@@ -0,0 +1,58 @@
machine:
python:
version: 3.5.1
node:
version: 6.1.0
dependencies:
pre:
- pip install invoke
override:
- invoke install
- invoke update_version
database:
override:
- createdb test
- createdb pivotal
- echo "CREATE ROLE pivotal WITH UNENCRYPTED PASSWORD 'password';" | psql -U postgres
- echo "ALTER ROLE pivotal WITH LOGIN;" | psql -U postgres
- echo "GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA test.public TO pivotal;" | psql -U postgres
- echo "GRANT CREATE, CONNECT ON DATABASE test TO pivotal;" | psql -U postgres
- echo "GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA pivotal.public TO pivotal;" | psql -U postgres
- echo "GRANT CREATE, CONNECT ON DATABASE pivotal TO pivotal;" | psql -U postgres
- PYTHONPATH=.:$PYTHONPATH alembic upgrade head
test:
override:
- invoke test_ci
deployment:
staging:
branch: master
commands:
- invoke update_heroku_version staging
- "[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow"
- git push -f git@heroku.com:sp-helix-staging.git $CIRCLE_SHA1:refs/heads/master
- heroku run --app sp-helix-staging invoke db_migrate
- rake ci:deliver
preprod:
branch: preprod
commands:
- invoke update_heroku_version preprod
- "[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow"
- git push -f git@heroku.com:sp-helix-preprod.git $CIRCLE_SHA1:refs/heads/master
- heroku run --app sp-helix-preprod invoke db_migrate
- rake ci:deliver
production:
tag: /release-.*/
commands:
- invoke update_heroku_version production
- "[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow"
- git push -f git@heroku.com:sp-helix-production.git $CIRCLE_SHA1:refs/heads/master
- heroku run --app sp-helix-production invoke db_migrate
- rake ci:deliver
notify:
webhooks:
- url: http://pulse.pivotallabs.com/projects/03ba990f-b8f5-4508-b4c1-19038b2cb791/status

1
db/README Normal file
View File

@@ -0,0 +1 @@
Generic single-database configuration.

142
db/env.py Normal file
View File

@@ -0,0 +1,142 @@
from __future__ import with_statement
import os
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
import logging
import re
USE_TWOPHASE = False
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# gather section names referring to different
# databases. These are named "engine1", "engine2"
# in the sample .ini file.
db_names = config.get_main_option('databases')
# add your model's MetaData objects here
# for 'autogenerate' support. These must be set
# up to hold just those tables targeting a
# particular database. table.tometadata() may be
# helpful here in case a "copy" of
# a MetaData is needed.
# from myapp import mymodel
# target_metadata = {
# 'engine1':mymodel.metadata1,
# 'engine2':mymodel.metadata2
#}
target_metadata = {}
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
# for the --sql use case, run migrations for each URL into
# individual files.
engines = {}
db_url = os.getenv('DATABASE_URL')
if db_url:
engines['heroku'] = {'url': db_url}
else:
for name in re.split(r',\s*', db_names):
engines[name] = rec = {}
rec['url'] = context.config.get_section_option(name,
"sqlalchemy.url")
for name, rec in engines.items():
logger.info("Migrating database %s" % name)
file_ = "%s.sql" % name
logger.info("Writing output to %s" % file_)
with open(file_, 'w') as buffer:
context.configure(url=rec['url'], output_buffer=buffer)
with context.begin_transaction():
context.run_migrations(engine_name=name)
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# for the direct-to-DB use case, start a transaction on all
# engines, then run all migrations, then commit all transactions.
engines = {}
db_url = os.getenv('DATABASE_URL')
if db_url:
engines['heroku'] = rec = {}
rec['engine'] = engine_from_config(
{'sqlalchemy.url': db_url},
poolclass=pool.NullPool)
else:
for name in re.split(r',\s*', db_names):
engines[name] = rec = {}
rec['engine'] = engine_from_config(
context.config.get_section(name),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
for name, rec in engines.items():
engine = rec['engine']
rec['connection'] = conn = engine.connect()
if USE_TWOPHASE:
rec['transaction'] = conn.begin_twophase()
else:
rec['transaction'] = conn.begin()
try:
for name, rec in engines.items():
logger.info("Migrating database %s" % name)
context.configure(
connection=rec['connection'],
upgrade_token="%s_upgrades" % name,
downgrade_token="%s_downgrades" % name,
target_metadata=target_metadata.get(name)
)
context.run_migrations()
if USE_TWOPHASE:
for rec in engines.values():
rec['transaction'].prepare()
for rec in engines.values():
rec['transaction'].commit()
except:
for rec in engines.values():
rec['transaction'].rollback()
raise
finally:
for rec in engines.values():
rec['connection'].close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
db/script.py.mako Normal file
View File

@@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,64 @@
"""Add ebom tables
Revision ID: 00817cda9d17
Revises: a904d0d1e1a7
Create Date: 2016-09-01 14:05:49.858683
"""
# revision identifiers, used by Alembic.
from sqlalchemy.orm import backref
from helix.constants.inverter_type import InverterType
revision = '00817cda9d17'
down_revision = 'a904d0d1e1a7'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
'power_stations',
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('site_id', sa.Integer, sa.ForeignKey('sites.id')),
sa.Column('quantity', sa.Integer, nullable=False),
sa.Column('ac_run_length', sa.Integer, nullable=False),
sa.Column('description', sa.Unicode, nullable=False),
)
op.create_table(
'standalone_inverters',
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('site_id', sa.Integer, sa.ForeignKey('sites.id')),
sa.Column('attachment_point_id', sa.Integer, sa.ForeignKey('power_stations.id')),
sa.Column('ac_run_length', sa.Integer, nullable=False),
)
op.create_table(
'inverters',
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('model', sa.Enum(str(InverterType.SMA.MODEL_12KW.value), str(InverterType.SMA.MODEL_15KW.value), str(InverterType.SMA.MODEL_20KW.value), str(InverterType.SMA.MODEL_24KW.value), name='InverterType'), nullable=False),
sa.Column('strings_per_inverter', sa.Integer, nullable=False),
sa.Column('sunshade', sa.Boolean),
sa.Column('dc_switch', sa.Boolean),
sa.Column('power_station_id', sa.Integer, sa.ForeignKey('power_stations.id')),
sa.Column('standalone_inverter_id', sa.Integer, sa.ForeignKey('standalone_inverters.id')),
)
op.create_table(
'power_monitors',
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('site_id', sa.Integer, sa.ForeignKey('sites.id')),
sa.Column('power_station_id', sa.Integer, sa.ForeignKey('power_stations.id'))
)
def downgrade():
op.drop_table('inverters')
op.drop_table('power_monitors')
op.drop_table('power_stations')
op.drop_table('standalone_inverters')
sa.Enum(name='InverterType').drop(op.get_bind(), checkfirst=False)

View File

@@ -0,0 +1,24 @@
"""Add splice box check on inverters
Revision ID: 1e472a2f3cbb
Revises: ed4c4bd22d6a
Create Date: 2017-07-10 12:20:44.492597
"""
# revision identifiers, used by Alembic.
revision = '1e472a2f3cbb'
down_revision = 'ed4c4bd22d6a'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('inverters', sa.Column('splice_box', sa.Boolean))
def downgrade():
op.drop_column('inverters', 'splice_box')

View File

@@ -0,0 +1,37 @@
"""Update Inverter models enum
Revision ID: 3cb6ab91fdc2
Revises: f90d04c490dc
Create Date: 2017-06-26 12:41:17.882779
"""
# revision identifiers, used by Alembic.
revision = '3cb6ab91fdc2'
down_revision = 'f90d04c490dc'
branch_labels = None
depends_on = None
from alembic import op
from helix.constants.inverter_type import InverterType
def upgrade():
op.execute('COMMIT')
connection = None
if not op.get_context().as_sql:
connection = op.get_bind()
connection.execution_options(isolation_level='AUTOCOMMIT')
op.execute('''ALTER TYPE "InverterType" ADD VALUE IF NOT EXISTS \'%s\'''' % (str(InverterType.DELTA.MODEL_36KW.value), ))
op.execute('''ALTER TYPE "InverterType" ADD VALUE IF NOT EXISTS \'%s\'''' % (str(InverterType.DELTA.MODEL_42KW.value), ))
op.execute('''ALTER TYPE "InverterType" ADD VALUE IF NOT EXISTS \'%s\'''' % (str(InverterType.DELTA.MODEL_60KW.value), ))
op.execute('''ALTER TYPE "InverterType" ADD VALUE IF NOT EXISTS \'%s\'''' % (str(InverterType.DELTA.MODEL_80KW.value), ))
if connection is not None:
connection.execution_options(isolation_level='READ_COMMITTED')
def downgrade():
pass

View File

@@ -0,0 +1,28 @@
"""add_file_names_to_site
Revision ID: 72342d883290
Revises: 00817cda9d17
Create Date: 2016-10-06 16:56:41.943103
"""
# revision identifiers, used by Alembic.
revision = '72342d883290'
down_revision = '00817cda9d17'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('sites', sa.Column('cad_file_name', sa.Unicode))
op.add_column('sites', sa.Column('dxf_file', sa.Unicode))
op.add_column('sites', sa.Column('dxf_file_name', sa.Unicode))
def downgrade():
op.drop_column('sites', 'cad_file_name')
op.drop_column('sites', 'dxf_file')
op.drop_column('sites', 'dxf_file_name')

View File

@@ -0,0 +1,56 @@
"""create initial tables
Revision ID: a904d0d1e1a7
Revises:
Create Date: 2016-08-31 11:16:58.286054
"""
# revision identifiers, used by Alembic.
revision = 'a904d0d1e1a7'
down_revision = None
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
'users',
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('username', sa.Unicode, nullable=False),
sa.Column('password_hash', sa.Unicode, nullable=False)
)
op.create_table(
'sites',
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('user_id', sa.Integer, sa.ForeignKey('users.id')),
sa.Column('project_name', sa.Unicode, nullable=False),
sa.Column('building_height', sa.Float, nullable=False),
sa.Column('building_width', sa.Float, nullable=False),
sa.Column('building_length', sa.Float, nullable=False),
sa.Column('parapet_height', sa.Float, nullable=False),
sa.Column('wind_speed', sa.Integer, nullable=False),
sa.Column('exposure_category', sa.Unicode, nullable=False),
sa.Column('exposure_transition_distance', sa.Integer),
sa.Column('ballast_block_weight', sa.Float, nullable=False),
sa.Column('max_psf', sa.Float, nullable=False),
sa.Column('system_type', sa.Enum('0', '1', name='SystemType'), nullable=False),
sa.Column('module_type', sa.Enum('96 Cell', '128 Cell', 'P-Series', name='ModuleType'), nullable=False),
sa.Column('anchor_type', sa.Enum('OMG PowerGrip', 'OMG PowerGrip Plus', 'EcoFasten Eco 65', name='AnchorType'), nullable=False),
sa.Column('spectral_response', sa.Float, nullable=False),
sa.Column('seismic_importance_factor', sa.Float, nullable=False),
sa.Column('cad_file', sa.Unicode),
)
def downgrade():
op.drop_table('sites')
op.drop_table('users')
sa.Enum(name='SystemType').drop(op.get_bind(), checkfirst=False)
sa.Enum(name='ModuleType').drop(op.get_bind(), checkfirst=False)
sa.Enum(name='AnchorType').drop(op.get_bind(), checkfirst=False)

View File

@@ -0,0 +1,39 @@
"""add cascade delete for inverter
Revision ID: ed4c4bd22d6a
Revises: 3cb6ab91fdc2
Create Date: 2017-06-30 11:10:24.762958
"""
# revision identifiers, used by Alembic.
revision = 'ed4c4bd22d6a'
down_revision = '3cb6ab91fdc2'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def upgrade():
op.drop_constraint('inverters_standalone_inverter_id_fkey', 'inverters', type_='foreignkey')
op.create_foreign_key(
'inverters_standalone_inverter_id_fkey',
'inverters',
'standalone_inverters',
['standalone_inverter_id'],
['id'],
ondelete='CASCADE'
)
def downgrade():
op.drop_constraint('inverters_standalone_inverter_id_fkey', 'inverters', type_='foreignkey')
op.create_foreign_key(
'inverters_standalone_inverter_id_fkey',
'inverters',
'standalone_inverters',
['standalone_inverter_id'],
['id']
)

View File

@@ -0,0 +1,30 @@
"""Add Inverter Brand table
Revision ID: f90d04c490dc
Revises: 72342d883290
Create Date: 2017-06-22 15:58:21.358210
"""
# revision identifiers, used by Alembic.
revision = 'f90d04c490dc'
down_revision = '72342d883290'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
'inverter_brands',
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('site_id', sa.Integer, sa.ForeignKey('sites.id'), primary_key=True),
)
pass
def downgrade():
op.drop_table('inverter_brands')
pass

23
docker-compose.yml Normal file
View File

@@ -0,0 +1,23 @@
version: '2'
services:
helix:
build: .
# ports:
# - "5000:5000" # flask server
# - "2222:2222" # ssh
volumes:
- .:/code
- /code/node_modules # http://jdlm.info/articles/2016/03/06/lessons-building-node-app-docker.html
network_mode: host
db:
build:
context: .
dockerfile: Dockerfile.db
volumes:
- /docker_data_volume/helix_roof_calculator_db:/var/lib/postgresql/data
network_mode: host
cache:
image: redis:3.2.4
volumes:
- /docker_data_volume/helix_roof_calculator_cache:/data
network_mode: host

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

View File

@@ -0,0 +1,42 @@
import copy
from helix.calculators.subarray_graph import SubarrayGraph, Direction
from helix.calculators.subarray_helper import extract_subarray
from helix.constants.panel_type import PanelType
class GraphRepository(object):
def __init__(self, panels, subarrays, system_type):
self.graphs = {}
for subarray in subarrays:
subarray_panels = extract_subarray(panels, subarray.subarray_number)
self.graphs[subarray.subarray_number] = SubarrayGraph(panels=subarray_panels, system_type=system_type)
def walk_callback(_, next_direction, direction_to_get_here):
if next_direction == Direction.North:
self.rows_perimeter += 1
elif next_direction == Direction.East:
self.columns_perimeter += 1
for subarray in subarrays:
graph = self.graphs[subarray.subarray_number]
subarray_panels = extract_subarray(panels, subarray.subarray_number)
rows_fallback = sum(1 for panel in subarray_panels if panel.panel_type == PanelType.Corner or panel.panel_type == PanelType.EastWest) / 2
columns_fallback = sum(1 for panel in subarray_panels if panel.panel_type == PanelType.Corner or panel.panel_type == PanelType.NorthSouth) / 2
if len(graph.nodes) == 0:
subarray.row_count = rows_fallback
subarray.column_count = columns_fallback
subarray.row_counted_geometrically = False
subarray.column_counted_geometrically = False
continue
self.rows_perimeter = 1
self.columns_perimeter = 1
graph.walk_graph_perimeter(graph.lower_left_node(graph.nodes), walk_callback, repeat_steps=False)
subarray.row_count = max(self.rows_perimeter, rows_fallback)
subarray.row_counted_geometrically = self.rows_perimeter >= rows_fallback
subarray.column_count = max(self.columns_perimeter, columns_fallback)
subarray.column_counted_geometrically = self.columns_perimeter >= columns_fallback
def subarray_graph(self, subarray_number):
return copy.deepcopy(self.graphs[subarray_number])

View File

View File

@@ -0,0 +1,22 @@
import json
class DocGenServiceError(Exception):
pass
class DocGenService(object):
def __init__(self, request_maker, request_builder):
self.request_maker = request_maker
self.request_builder = request_builder
def generate(self):
url = 'https://dcs.us.sunpower.com/ws/docgen/docx/generatePdf'
headers = {'content-type': 'application/json'}
params = json.dumps(self.request_builder.build())
result = self.request_maker.post(url, params, headers=headers)
if result.status_code != 200:
raise DocGenServiceError(result.content)
return result.content

1002
helix/Services/dxf_helper.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,94 @@
import io
import dxfgrabber
from helix.constants.file_validation_error import FileValidationMessage
from helix.models.dxf.dxf_error import OldDxfFormatException
class DXFService(object):
"""
Takes the contents of a DXF file and creates modules, buildings, panels,
subarrays, and polygons based on the data
"""
# l_b is expected to be in inches, not feet!
def parse(self, dxf_file_contents, module_constants, system_type, l_b, dxf_helper, subarray_validator):
"""Parse will generate the panels, subarrays, buildings
and modules that are present in the dxf file
Arguments:
dxf_file_contents (str) Content of the uploaded file
"""
dxf = dxfgrabber.read(io.StringIO(dxf_file_contents, newline=None))
buildings, modules = dxf_helper.build_polygons(dxf.entities)
"""
A new type of aurora format file was added in the project
The visibile difference if you read the file, is that the
new format contains the string buildings.
The old version doesn't
"""
pair_spacing = None
dxf_helper.is_new_aurora_format()
if hasattr(module_constants, "spacing_size_inches"):
pair_spacing = module_constants.spacing_size_inches
if dxf_helper.should_consolidate_modules(modules, system_type,
module_constants):
modules = dxf_helper.consolidate_dual_tilt_modules(modules,
system_type,
pair_spacing)
translated_buildings, translated_modules = dxf_helper.translate_towards_origin(buildings, modules)
translated_buildings_ccw = dxf_helper.get_polygons_counterclockwise(translated_buildings)
buildings_ccw = dxf_helper.get_polygons_counterclockwise(buildings)
panels = dxf_helper.generate_panels(modules, translated_modules)
node_graph = dxf_helper.build_node_graph(panels, module_constants.panel_spacing)
subarrays = dxf_helper.detect_subarrays(node_graph, panels)
for subarray in subarrays:
subarray_validator.validate_subarray(node_graph, subarray.subarray_number, system_type)
dxf_helper.detect_panel_types(node_graph)
dxf_helper.detect_wind_zones(panels, translated_buildings_ccw, translated_modules, l_b, system_type)
all_points = [p.points for p in translated_buildings_ccw + translated_modules]
points = [point for points_list in all_points for point in points_list]
max_x = max(p[0] for p in points)
max_y = max(p[1] for p in points)
panel_orientation = panels[0].coordinate.rotation
return {
'size': (max_x, max_y), # Used for debugging
'buildings': buildings_ccw,
'modules': translated_modules,
'panels': panels,
'subarrays': subarrays,
'lb_polygons': dxf_helper.l_b_polygons(translated_buildings_ccw, l_b, system_type, panel_orientation),
'is_panel_drawing_inaccurate': self.is_panel_drawing_inaccurate(panels)
}
def is_panel_drawing_inaccurate(self,panels):
'''True if subarrays are not rotated more than allowed tolerance, false otherwise'''
ROTATION_ACCURACY_DELTA_DEGREES = 0.1
if panels is None or panels == []:
return True
first_panel_rotation = panels[0].coordinate.rotation # rotation is in degrees
for panel in panels:
difference = abs(first_panel_rotation - panel.coordinate.rotation)
if difference >= ROTATION_ACCURACY_DELTA_DEGREES:
return True
return False

0
helix/__init__.py Normal file
View File

1
helix/api/__init__.py Normal file
View File

@@ -0,0 +1 @@
__author__ = 'pivotal'

65
helix/api/api.py Normal file
View File

@@ -0,0 +1,65 @@
from flask import Blueprint, request, session, jsonify
from helix.calculators.calculator import Calculator
from helix.presenters.panel_presenter import ProjectPresenter
from helix.session_manager import SessionManager
from helix.constants import redis_constant, sql_constant
from helix.seismic_validator_user_values import SeismicValidatorUserValues
from helix.validators.file_validator import FileValidator
from helix.validators.seismic_anchor_validator import SeismicAnchorValidator
api = Blueprint('api', __name__, template_folder='templates')
@api.route("/panel_data")
def panel_data():
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
user_values = session_manager.user_values()
calculator = Calculator(user_values)
system_type = user_values.system_type()
module_type = user_values.module_type()
data = ProjectPresenter(system_type, module_type).get_panel_data(calculator.get_computed_csv_columns(), calculator.subarrays)
db_session.close()
return jsonify({'panel_data': data})
@api.route("/update_panel_data", methods=['POST'])
def update_panel_data():
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
user_seismic_data = request.get_json()
user_values = SeismicValidatorUserValues(session_manager.user_values(), user_seismic_data)
calculator = Calculator(user_values)
validator = SeismicAnchorValidator(calculator)
validation_result = validator.validate(calculator.panels)
subarrays = calculator.subarray_summary()
subarray_data = []
for subarray in subarrays:
data = {
"subarray": subarray.subarray_number,
"required_seismic_anchors": subarray.required_seismic_anchors,
"weight": round(subarray.weight),
}
subarray_data.append(data)
if not validation_result:
session_manager.save_user_provided_seismic_anchors(user_seismic_data)
panel_data = ProjectPresenter(user_values.system_type(), user_values.module_type()).get_panel_data(calculator.get_computed_csv_columns(),
calculator.subarrays)
db_session.close()
return jsonify({
"status": "success",
"error": None,
"panel_data": panel_data,
"subarray_data": subarray_data
})
else:
db_session.close()
return jsonify({
"status": "error",
"error": validation_result.value,
"panel_data": None,
"subarray_data": subarray_data
})

View File

@@ -0,0 +1,78 @@
import json
from numpy import array
class CalculatedDataRepository(object):
summary_values_key = 'summary_values'
computed_csv_columns_key = 'computed_csv_columns'
compute_bom_key = 'compute_bom'
subarray_summary_key = 'subarray_summary'
def __init__(self, store, calculator):
self.store = store
self.calculator = calculator
def reset_data(self):
pass
def reset_site_data(self):
pass
def reset_panel_data(self):
pass
def reset_subarray_data(self):
self.store.delete(self.summary_values_key)
self.store.delete(self.subarray_summary_key)
def reset_ebom_data(self):
self.store.delete(self.compute_bom_key)
def k_z(self):
return self.calculator.k_z()
def L_B(self):
return self.calculator.L_B()
def summary_table(self):
return self.calculator.summary_table()
def minimum_array_sizes(self):
return self.calculator.minimum_array_sizes()
def summary_values(self):
key = self.summary_values_key
if self.store.exists(key):
summary_values_json = self.store.get(key).decode('utf-8)')
return array(json.loads(summary_values_json))
summary_values = self.calculator.summary_values()
self.store.set(key, json.dumps(list(summary_values)))
return summary_values
def get_computed_csv_columns(self):
return self.calculator.get_computed_csv_columns() # TODO: revisit after getting rid of DataMatrix
# key = self.computed_csv_columns_key
# if self.store.exists(key):
# computed_csv_columns_json = self.store.get(key).decode('utf-8')
# return jsonpickle.decode(computed_csv_columns_json)
# computed_csv_columns = self.calculator.get_computed_csv_columns()
# self.store.set(key, jsonpickle.encode(computed_csv_columns))
# return computed_csv_columns
def compute_bom(self):
key = self.compute_bom_key
if self.store.exists(key):
compute_bom_json = self.store.get(key).decode('utf-8')
return array(json.loads(compute_bom_json))
computed_bom = self.calculator.compute_bom()
self.store.set(key, json.dumps(computed_bom.tolist()))
return computed_bom
def subarray_summary(self):
key = self.subarray_summary_key
if self.store.exists(key):
received_json = self.store.get(key).decode('utf-8')
return array(json.loads(received_json))
data = self.calculator.subarray_summary()
self.store.set(key, json.dumps(data.tolist()))
return data

View File

View File

@@ -0,0 +1,248 @@
from collections import namedtuple, OrderedDict
from math import ceil, floor
from helix.constants.panel_type import PanelType
from helix.constants.system_type import SystemType
from helix.models.panel import Panel, PanelWarnings
Result = namedtuple('Result', ['ballast_count', 'link_tray_count', 'cross_tray_count', 'system_weight', 'needs_anchor'])
class BallastCalculator(object):
def __init__(self, user_values):
self.values = user_values
self.system_type = user_values.system_type()
self.anchor_type = user_values.anchor_type()
self.system_constants = self.system_type.system_constants()
self.module_constants = user_values.module_system_constants()
def ballast_and_trays_matrix(self, c_p_matrix, q_z, panels, ballast_block_weight=None):
if not ballast_block_weight:
ballast_block_weight = self.values.ballast_block_weight()
ballast_store = self.calculate_ballast_store(c_p_matrix, q_z, ballast_block_weight)
for idx, panel in enumerate(panels):
stored_panel = ballast_store[panel.panel_type][panel.wind_zone][panel.fuzzy_wind_zone]
panels[idx] = stored_panel.merge(panel)
return panels
def update_ballast(self, c_p_matrix, q_z, panels):
ballast_block_weight = self.values.ballast_block_weight()
ballast_store = self.calculate_ballast_store(c_p_matrix, q_z, ballast_block_weight)
seismic_ballast_store = {}
for panel in panels:
seismic_anchors = panel.seismic_anchors if panel.seismic_anchors else 0
if seismic_anchors != 0:
key = hash(panel.wind_zone) + hash(panel.panel_type) + seismic_anchors + hash(panel.fuzzy_wind_zone) # hack
stored_panel = seismic_ballast_store.get(key)
if stored_panel:
panel.ballast = stored_panel.ballast
panel.link_tray = stored_panel.link_tray
panel.cross_tray = stored_panel.cross_tray
panel.pressure = stored_panel.pressure
else:
anchors = panel.wind_anchors + seismic_anchors
c_p = c_p_matrix[panel.wind_zone, panel.panel_type.index()] * (1.15 if panel.fuzzy_wind_zone else 1)
force = self.uplift(c_p, q_z) - anchors * self.anchor_type.uplift_capacity()
ballast_and_tray_count = self.ballast_and_tray_count(force, panel.panel_type, ballast_block_weight, anchors)
pressure = self.calculate_pressure_on_roof(ballast_and_tray_count.ballast_count, ballast_block_weight, ballast_and_tray_count.system_weight)
panel.ballast = ballast_and_tray_count.ballast_count
panel.link_tray = ballast_and_tray_count.link_tray_count
panel.cross_tray = ballast_and_tray_count.cross_tray_count
panel.pressure = pressure
seismic_ballast_store[key] = panel
else:
stored_panel = ballast_store[panel.panel_type][panel.wind_zone][panel.fuzzy_wind_zone]
panel.ballast = stored_panel.ballast
panel.link_tray = stored_panel.link_tray
panel.cross_tray = stored_panel.cross_tray
panel.pressure = stored_panel.pressure
return panels
def calculate_ballast_store(self, cp_matrix, qz, ballast_block_weight):
max_psf = self.values.max_system_pressure()
store = {}
for panel_type in PanelType.all():
sub_store = {}
for wind_zone, _ in enumerate(self.values.system_type().system_constants().wind_zones):
sub_store[wind_zone] = {}
for use_fuzzy in (True, False):
sub_store[wind_zone][use_fuzzy] = self.ballast_tray_and_anchor_count(wind_zone=wind_zone,
panel_type=panel_type,
ballast_block_weight=ballast_block_weight,
max_system_pressure=max_psf,
c_p_matrix=cp_matrix,
q_z=qz,
use_fuzzy=use_fuzzy)
store[panel_type] = sub_store
return store
def summary_table(self, c_p_matrix, q_z):
wind_zones = self.system_constants.wind_zones
ballast_block_weight = self.values.ballast_block_weight()
max_system_pressure = self.values.max_system_pressure()
table = OrderedDict()
for panel_type in PanelType.all():
ballast_counts = []
anchor_counts = []
pressures = []
warnings = []
for wind_zone_index, _ in enumerate(wind_zones):
ballast_tray_anchor_panels = self.ballast_tray_and_anchor_count(wind_zone_index, panel_type,
ballast_block_weight, max_system_pressure,
c_p_matrix, q_z)
anchor_count = ballast_tray_anchor_panels.wind_anchors
ballast_count = ballast_tray_anchor_panels.ballast
pressure = ballast_tray_anchor_panels.pressure
warning = ballast_tray_anchor_panels.warnings
pressure_as_string = "{0:.2f}".format(pressure)
# Because pressure is stored as a floating point number, it is possible, because floats
# for pressure to be something like 5.02999999999999. Which is clearly meant to be 5.03.
# This represents that as a string, which doesn't have that issue.
anchor_counts.append(anchor_count)
ballast_counts.append(ballast_count)
pressures.append(pressure_as_string)
warnings.append(warning)
table[panel_type] = {
'ballast blocks': ballast_counts,
'anchors': anchor_counts,
'pressure': pressures,
'warnings': warnings
}
return table
def ballast_tray_and_anchor_count(self, wind_zone, panel_type, ballast_block_weight, max_system_pressure,
c_p_matrix, q_z, use_fuzzy=False):
fuzzy_factor = 1.15 if use_fuzzy else 1
c_p = c_p_matrix[wind_zone, panel_type.index()] * fuzzy_factor
uplift_force = self.uplift(c_p, q_z)
warnings = []
keep_trying = True
anchor_count = 0
pressure = 0.
tries = 0
ballast_and_tray_count = None
while keep_trying:
remainder_force = uplift_force - anchor_count * self.anchor_type.uplift_capacity()
ballast_and_tray_count = self.ballast_and_tray_count(remainder_force, panel_type, ballast_block_weight, anchor_count)
pressure = self.calculate_pressure_on_roof(ballast_and_tray_count.ballast_count, ballast_block_weight, ballast_and_tray_count.system_weight)
keep_trying = (ballast_and_tray_count.needs_anchor or pressure > max_system_pressure) and ballast_and_tray_count.ballast_count > 0
if keep_trying:
anchor_count = self.calculate_anchors(panel_type, uplift_force) + tries
tries += 1
keep_trying &= tries < 100
if uplift_force / self.module_constants.surface_area >= self.module_constants.max_psf:
warnings.append(PanelWarnings.MaxPsf)
return Panel(wind_zone=wind_zone,
panel_type=panel_type,
ballast=ballast_and_tray_count.ballast_count,
link_tray=self.interpret_tray_count(ballast_and_tray_count.link_tray_count, panel_type),
cross_tray=ballast_and_tray_count.cross_tray_count,
wind_anchors=anchor_count,
pressure=pressure,
fuzzy_wind_zone=use_fuzzy,
warnings=warnings)
def ballast_and_tray_count(self, force_to_resist, panel_type, ballast_block_weight, anchor_count):
system_weight = self.module_constants.base_weight(panel_type, 0)
ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight)
link_tray_count = 0
cross_tray_count = 0
needs_anchor = False
keep_trying = True
tries = 0
while keep_trying and tries < 3:
tries += 1
if ballast_count:
new_link_tray_count, _ = self.calculate_trays(ballast_count + 2 * anchor_count,
self.module_constants.link_tray_thresholds(panel_type))
# Recalculate weight given new link trays; recalculate ballast given new weight
system_weight = self.module_constants.base_weight(panel_type, new_link_tray_count + cross_tray_count)
ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight)
new_cross_tray_count, needs_anchor = self.calculate_trays(ballast_count + 2 * anchor_count,
self.module_constants.cross_tray_thresholds(
panel_type))
system_weight = self.module_constants.base_weight(panel_type, new_cross_tray_count + new_link_tray_count)
ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight)
if link_tray_count == new_link_tray_count and cross_tray_count == new_cross_tray_count:
keep_trying = False
link_tray_count = new_link_tray_count
cross_tray_count = new_cross_tray_count
else:
keep_trying = False
return Result(ballast_count, link_tray_count=link_tray_count, cross_tray_count=cross_tray_count,
system_weight=system_weight, needs_anchor=needs_anchor)
def uplift(self, c_p, q_z):
return q_z * self.module_constants.surface_area * c_p
def calculate_ballast(self, uplift, non_ballast_weight, ballast_block_weight):
if non_ballast_weight > uplift:
return 0
return ceil((uplift - non_ballast_weight) / ballast_block_weight)
def calculate_trays(self, ballast_count, thresholds):
for idx, threshold in enumerate(thresholds):
if ballast_count <= threshold:
return idx, False
return len(thresholds) - 1, True
def calculate_pressure_on_roof(self, ballast_count, ballast_block_weight, non_ballast_weight):
effective_area = self.module_constants.surface_area / self.module_constants.ground_coverage_ratio
return (ballast_count * ballast_block_weight + non_ballast_weight) / effective_area
def interpret_tray_count(self, link_tray_count, panel_type):
if self.system_type == SystemType.singleTilt:
if panel_type == PanelType.EastWest:
return 2
elif panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return 0
return link_tray_count or 0
def calculate_anchors(self, panel_type, uplift):
base_system_weight = self.module_constants.base_weight(panel_type, 0)
anchor_capacity = self.anchor_type.uplift_capacity()
return max(floor((uplift - base_system_weight) / anchor_capacity), 1)
def show_presented_link_trays(self, panels):
for panel in panels:
panel.presented_link_tray = self.present_link_tray(panel.link_tray, panel.panel_type)
return panels
def present_link_tray(self, link_tray_count, panel_type):
if self.system_type == SystemType.singleTilt:
link_tray_representation = {
PanelType.Corner: 0,
PanelType.NorthSouth: 0,
PanelType.EastWest: 2,
PanelType.Middle: min(1, int(link_tray_count)),
}[panel_type]
else:
link_tray_representation = {
PanelType.Corner: 2,
PanelType.NorthSouth: 2,
PanelType.EastWest: 1,
PanelType.Middle: min(1, int(link_tray_count)),
}[panel_type]
return link_tray_representation

View File

@@ -0,0 +1,58 @@
from numpy import array, ceil
from helix.calculators.bom_helper import add_parts_to_list, apply_package_size_rounding
from helix.calculators.ebom_calculator import EbomCalculator
from helix.calculators.mechanical_bom_calculator import MechanicalBomCalculator
from helix.constants.parts import *
class BomCalculator(object):
def __init__(self, values, panels, subarrays, graph_repository):
self.values = values
self.panels = panels
self.subarrays = subarrays
self.graph_repository = graph_repository
def compute_bom(self):
output_array = []
for part, quantity in self.parts_list().items():
if ceil(quantity) <= 0:
continue
row = list(part)
row.append(int(ceil(quantity)))
output_array.append(row)
output_array.sort(key=lambda x: x[0] + x[1])
return array(output_array)
def documentation_bom(self):
parts_list = self.parts_list()
for part in all_parts:
if part not in parts_list.keys():
parts_list[part] = 0
output_array = []
for part, quantity in parts_list.items():
if part == ballast:
row = 'ballast'
elif part == anchor:
row = 'anchors'
elif part == module:
row = 'modules'
else:
row = part[0]
quantity = max(0, quantity)
output_array.append((row, int(ceil(quantity))))
return output_array
def parts_list(self):
row_count = sum(subarray.row_count for subarray in self.subarrays)
column_count = sum(subarray.column_count for subarray in self.subarrays)
parts_list = MechanicalBomCalculator(self.values, self.panels, self.subarrays).mechanical_bom()
ebom_parts_list = EbomCalculator(self.values, ceil(row_count), ceil(column_count), parts_list.get(module)).compute_ebom()
add_parts_to_list(parts_list, ebom_parts_list)
apply_package_size_rounding(parts_list, package_sizes)
return parts_list

View File

@@ -0,0 +1,36 @@
from math import ceil
from helix.constants.panel_type import PanelType
def add_parts_to_list(parts_list, parts_to_add, multiplier=1):
for part, quantity in parts_to_add.items():
previous_value = parts_list.get(part) or 0
if quantity != 0:
parts_list[part] = previous_value + quantity * multiplier
def apply_fudge_factors(parts_list, fudge_factors):
for part, quantity in parts_list.items():
fudge_factor = fudge_factors.get(part) or 1.0
parts_list[part] = quantity * fudge_factor
def apply_package_size_rounding(parts_list, package_sizes):
for part, quantity in parts_list.items():
package_size = package_sizes.get(part) or 1
parts_list[part] = package_size * ceil(quantity / package_size)
def get_panel_type_counts(panels):
panel_type_counts = {
PanelType.Corner: 0,
PanelType.NorthSouth: 0,
PanelType.EastWest: 0,
PanelType.Middle: 0,
}
for panel in panels:
panel_type_counts[panel.panel_type] += 1
return panel_type_counts

View File

@@ -0,0 +1,169 @@
from math import ceil, floor
import copy
from helix.Repositories.graph_repository import GraphRepository
from helix.calculators.ballast_calculator import BallastCalculator
from helix.calculators.bom_calculator import BomCalculator
from helix.calculators.coordinates_calculator import CoordinatesCalculator
from helix.calculators.pressure_coefficient_calculator import PressureCoefficientCalculator
from helix.calculators.seismic_calculator import SeismicCalculator
from helix.calculators.subarray_helper import get_subarray_sizes_and_rows, extract_subarray
from helix.calculators.summary_values_calculator import SummaryValuesCalculator
from helix.calculators.wind_pressure_calculator import WindPressureCalculator
class Calculator(object):
def __init__(self, user_values, calculate_panel_data=True):
self.values = user_values
self._q_z = None
self._c_p_matrix = None
self._L_B = None
self._K_z = None
self.subarrays = None
self.buildings = self.values.buildings_polygons()
self.buildings_for_drawing = []
self.panels = self.values.csv()
if calculate_panel_data and self.panels is not None:
for idx, panel in enumerate(self.panels):
panel.id = idx + 1
self.panels.sort(key=lambda x: x.subarray)
self.subarrays = get_subarray_sizes_and_rows(self.panels)
self.__compute_ballast()
_,_,self.buildings_for_drawing = self.__transform_coordinates()
self.graph_repository = GraphRepository(self.panels, self.subarrays, self.values.system_type())
if self.values.user_override_seismic_anchors():
user_provided_panels = self.values.get_user_provided_seismic_anchors()
for user_panel in user_provided_panels:
panel = [panel for panel in self.panels if panel.id == user_panel.id][0]
panel.seismic_anchors = user_panel.seismic_anchors
self.__compute_seismic_anchors(self.panels) # Update subarrays to include required seismic anchors
self.__update_ballast(self.panels)
else:
# Update subarrays *and panels* to include required seismic anchors
self.panels = self.__compute_seismic_anchors(self.panels)
def k_z(self):
if self._K_z is None:
self._K_z = WindPressureCalculator(self.values).K_z()
return self._K_z
def L_B(self):
if self._L_B is None:
self._L_B = PressureCoefficientCalculator(self.values).L_B()
return self._L_B
def summary_table(self):
return BallastCalculator(self.values).summary_table(self.__c_p_matrix(), self.q_z())
def minimum_array_sizes(self):
return PressureCoefficientCalculator(self.values).minimum_array_size(self.L_B())
# Used in the array summary page - is the table of weight, psf, anchors, ballast, etc. for the entire system
def summary_values(self):
seismic_anchors = self.subarray_summary()
ballast_calculator = BallastCalculator(self.values)
seismic_interval = SeismicCalculator(self.values, self.graph_repository).seismic_anchor_interval()
return SummaryValuesCalculator(self.values).summary_values(self.panels, seismic_anchors, self.__c_p_matrix(),
self.q_z(), seismic_interval, ballast_calculator)
def documentation_summary_values(self):
seismic_anchors = self.subarray_summary()
ballast_calculator = BallastCalculator(self.values)
seismic_interval = SeismicCalculator(self.values, self.graph_repository).seismic_anchor_interval()
return SummaryValuesCalculator(self.values).documentation_summary_values(self.panels, seismic_anchors, self.__c_p_matrix(),
self.q_z(), seismic_interval, ballast_calculator)
# used in the array visualization - is parsed into json and displayed using the fancy canvas
def get_computed_csv_columns(self):
return BallastCalculator(self.values).show_presented_link_trays(self.panels)
def compute_bom(self):
required_seismic_anchors = self.subarray_summary()
return BomCalculator(self.values, self.panels, required_seismic_anchors, self.graph_repository).compute_bom()
def documentation_bom(self):
required_seismic_anchors = self.subarray_summary()
return BomCalculator(self.values, self.panels, required_seismic_anchors, self.graph_repository).documentation_bom()
# used in the array summary page - is part of the fancy scrolling table of summing up each subarray
def subarray_summary(self):
summary_values_calculator = SummaryValuesCalculator(self.values)
for subarray in self.subarrays:
panels_for_subarray = extract_subarray(self.panels, subarray.subarray_number)
weight, _ = summary_values_calculator.system_weight_and_pressure(panels_for_subarray)
subarray.weight = weight
return self.subarrays
def q_z(self):
if self._q_z is None:
self._q_z = WindPressureCalculator(self.values).q_z(self.k_z())
return self._q_z
def __c_p_matrix(self):
if self._c_p_matrix is None:
self._c_p_matrix = PressureCoefficientCalculator(self.values).c_p_matrix(self.L_B())
return self._c_p_matrix
def __compute_seismic_anchors(self, panels):
panels = copy.deepcopy(panels)
seismic_calculator = SeismicCalculator(self.values, self.graph_repository)
for subarray in self.subarrays:
if subarray.required_seismic_anchors is None:
subarray.required_seismic_anchors = 0
panels = self.__seismic_anchors_for_subarray(panels, subarray, seismic_calculator)
return panels
def __seismic_anchors_for_subarray(self, panels, subarray, seismic_calculator):
# do first estimation to obtain upper bound
required_seismic = seismic_calculator.required_force_seismic_anchors(subarray.subarray_number, panels)
test_value = required_seismic
tried_acceptable_values = []
def assign_seismic_anchors(count):
subarray.required_seismic_anchors = count
seismic_calculator.assign_seismic_anchors(subarray, panels)
self.__update_ballast(panels)
assigned = sum([panel.seismic_anchors for panel in panels if panel.seismic_anchors is not None])
return assigned
step = max(1, test_value // 2)
while True:
seismic_anchors_assigned = assign_seismic_anchors(test_value)
provided_force = seismic_calculator.compute_provided_lateral_capacity(subarray.subarray_number, panels)
required_seismic_force = seismic_calculator.required_force_seismic_demand(subarray.subarray_number, panels)
if seismic_anchors_assigned == 0:
# anchors were not assigned propably because self.graph_repository.subarray_graph(subarray.subarray_number) is empty
# which may be because of test construction
return panels
if provided_force < required_seismic_force:
test_value += step
else:
if (test_value in tried_acceptable_values) or (required_seismic_force == 0):
return panels
tried_acceptable_values.append(test_value)
test_value = max(0, test_value - step)
step = max(1, step // 2)
def __transform_coordinates(self):
return CoordinatesCalculator(self.values).transform_coordinates(self.panels, self.subarrays, self.buildings)
def __compute_ballast(self):
ballast_calculator = BallastCalculator(self.values)
changed_panels = ballast_calculator.ballast_and_trays_matrix(self.__c_p_matrix(), self.q_z(), self.panels)
self.panels = [panel.merge(changed_panels[idx]) for idx, panel in enumerate(self.panels)]
def __update_ballast(self, panels):
BallastCalculator(self.values).update_ballast(self.__c_p_matrix(), self.q_z(), panels)

View File

@@ -0,0 +1,104 @@
from numpy import array, math, dot, vectorize
from helix.calculators.subarray_helper import extract_subarray
from helix.models.coordinate import Coordinate
class CoordinatesCalculator(object):
def __init__(self, values):
self.values = values
def transform_coordinates(self, panels, subarrays, buildings):
"""
Scales, rotates, and translates the coordinates so that they're all
in inches, and in positive unit space.
Coordinates are rounded to whole values (used in drawing on the
array_summary page
Parameters:
panels (obj): List of panels
subarrays (obj): List of subarrays
buildings (obj): List of lists of building polygons
Returns:
tupple
"""
rotate_all = vectorize(self.rotate)
scale_all = vectorize(self.scale)
round_all = vectorize(round)
neg_translate_all = vectorize(self.neg_translate)
origins = []
first_subarray_rotation = None
for subarray in subarrays:
begin, size = (subarray.start_row, subarray.size)
extracted_panels = extract_subarray(panels, subarray.subarray_number)
raw_coordinates = [panel.coordinate for panel in extracted_panels]
if first_subarray_rotation is None:
first_subarray_rotation = raw_coordinates[0].rotation
rotated_coordinates = rotate_all(raw_coordinates)
scaled_coordinates = scale_all(rotated_coordinates)
origin = self.find_origin(scaled_coordinates)
rounded_coordinates = round_all(scaled_coordinates - origin)
for idx, val in enumerate(rounded_coordinates):
panels[begin + idx].coordinate = val
origins.append(origin)
prepared_buildings = self.prepare_buildings(buildings, first_subarray_rotation)
rotated_buildings = list(map(lambda building: rotate_all(building), prepared_buildings ))
scaled_buildings = list(map(lambda building: scale_all(building), rotated_buildings))
all_building_coordinates = [point for sublist in scaled_buildings for point in sublist]
global_origin = self.find_origin(all_building_coordinates + origins)
origins = array(origins) - global_origin
for idx, origin in enumerate(origins):
subarrays[idx].origin = origin
translated_buildings = list(map(lambda building: neg_translate_all(building, global_origin), scaled_buildings))
# rounded_buildings = list(map(lambda building: round_all(building), translated_buildings))
return panels, subarrays, translated_buildings
def rotate(self, coordinate):
rotation = math.radians(coordinate.rotation)
rotation_matrix = array([[math.cos(rotation), -math.sin(rotation)],
[math.sin(rotation), math.cos(rotation)]])
vector = (coordinate.x, coordinate.y)
rotated_vector = dot(vector, rotation_matrix)
return Coordinate(rotated_vector[0], rotated_vector[1])
def scale(self, coordinate):
constants = self.values.module_system_constants()
panel_x, panel_y = constants.panel_spacing
return coordinate.scale(1. / panel_x, 1. / panel_y)
def neg_translate(self, coordinate, other):
return coordinate.neg_translate(other)
def find_origin(self, coordinates):
if coordinates == []:
return Coordinate(0,0)
min_x = min(list(map(lambda x: x.x, coordinates)))
min_y = min(list(map(lambda x: x.y, coordinates)))
return Coordinate(min_x, min_y)
def prepare_buildings(self, buildings, rotation):
return list(map(lambda building: self.prepare_single_building(building, rotation), buildings))
def prepare_single_building(self, building_array, rotation):
return list(map(lambda point: Coordinate(point[0],point[1],rotation), building_array))

View File

@@ -0,0 +1,133 @@
from math import ceil
from helix.calculators.bom_helper import add_parts_to_list
from helix.constants import ebom_parts
from helix.constants.ebom_parts import *
from helix.constants.parts import wire_clip_large, cable_support, cable_support_lid, channel_nut, sunshade
from helix.constants.system_type import SystemType
class EbomCalculator(object):
def __init__(self, user_values, row_count, column_count, modules_count = None):
self.values = user_values
self.row_count = row_count
self.column_count = column_count
self.modules_count = modules_count
def resolve_power_monitor_type(self):
module_type = self.values.module_type()
thresholds = {
ModuleType.Cell96: 306,
ModuleType.Cell128: 230,
ModuleType.PSeries: 286
}
if (not self.modules_count) or self.modules_count >= thresholds[module_type]:
return monitor_controller_480_v
else:
return monitor_controller_240_v
def compute_ebom(self):
part_list = {}
power_stations = self.values.power_stations()
standalone_inverters = self.values.standalone_inverters()
monitors = self.values.power_monitors()
module_type = self.values.module_type()
system_type = self.values.system_type()
inverter_count = 0
total_ac_run_length = 0
panel_board_counts = [0, 0]
proper_monitor_controller = self.resolve_power_monitor_type()
for power_station in power_stations:
power_station_count = power_station['power_station_quantity']
total_ac_run_length += power_station['ac_run_length']
inverter_quantity = power_station['inverter_quantity'] + self.get_standalone_inverters(power_station)
if inverter_quantity <= 2:
panel_board_counts[0] += power_station_count
else:
panel_board_counts[1] += power_station_count
if self.power_station_has_monitor(power_station, monitors):
panel_board_parts_to_use = panel_board_parts_with_monitor(inverter_quantity, proper_monitor_controller)
else:
panel_board_parts_to_use = panel_board_parts(inverter_quantity, with_aux=False)
add_parts_to_list(part_list, panel_board_parts_to_use, power_station_count)
add_parts_to_list(part_list, shared_panel_board_parts(module_type, system_type), power_station_count)
add_parts_to_list(part_list, {channel_nut: 4}, power_station_count)
for inverter in power_station['inverters']:
inverter_count += power_station_count
self.add_parts_for_inverter(part_list, inverter, power_station_count)
add_parts_to_list(part_list, inverter_parts(inverter, module_type), power_station_count)
for inverter in standalone_inverters:
inverter_count += 1
total_ac_run_length += inverter['ac_run_length']
self.add_parts_for_inverter(part_list, inverter)
add_parts_to_list(part_list, standalone_inverter_parts(inverter, system_type, module_type), 1)
add_parts_to_list(part_list, inverter_parts(inverter, module_type), 1)
if inverter['attachment_point'][1]:
add_parts_to_list(part_list, standalone_inverter_attached_to_panel_board_parts, 1)
for monitor in monitors:
if monitor['power_source'][0] == 'Switch Gear/External':
add_parts_to_list(part_list, {proper_monitor_controller: 1}, 1)
add_parts_to_list(part_list, {wire_clip_large: inverter_count}, self.row_count)
add_parts_to_list(part_list, {stump: 1}, ceil(total_ac_run_length / 4.0))
cable_supports = self.calculate_cable_supports(panel_board_counts, len(standalone_inverters))
add_parts_to_list(part_list, {cable_support: 1, cable_support_lid: 1}, cable_supports)
add_parts_to_list(part_list, {rear_skirt: -1}, ceil(cable_supports*.38))
dependent_part_list = {}
for part, quantity in part_list.items():
dependent_parts = ebom_parts.dependent_parts(module_type, system_type).get(part)
if dependent_parts:
add_parts_to_list(dependent_part_list, dependent_parts, quantity)
add_parts_to_list(part_list, dependent_part_list)
return part_list
def add_parts_for_inverter(self, part_list, inverter, multiplier=1):
strings_per_inverter = inverter_strings_parts.get(inverter['strings_per_inverter'], {})
add_parts_to_list(part_list, inverter_model_parts[inverter['model']], multiplier)
add_parts_to_list(part_list, strings_per_inverter, multiplier)
if inverter['sunshade']:
add_parts_to_list(part_list, {sunshade: 1, sunshade_bolt: 2, sunshade_washer: 2}, multiplier)
if inverter['dc_switch']:
add_parts_to_list(part_list, dc_switch_parts, multiplier)
def calculate_cable_supports(self, panel_board_counts, standalone_inverter_count):
if sum(panel_board_counts) == 0:
return 0
if self.values.system_type() == SystemType.dualTilt:
dimension1 = self.column_count
dimension2 = self.row_count
else:
dimension1 = self.row_count
dimension2 = self.column_count
result = (standalone_inverter_count + panel_board_counts[0] + (2 * panel_board_counts[1])) / sum(panel_board_counts)
result *= dimension1 * max(dimension1 / dimension2, 1)
result += dimension1
return ceil(result)
def get_standalone_inverters(self, power_station):
standalone_inverters = self.values.standalone_inverters()
count = 0
for inverter in standalone_inverters:
if inverter['attachment_point'][1] == power_station['power_station_id']:
count += 1
return count
def power_station_has_monitor(self, power_station, monitors):
for monitor in monitors:
if monitor['power_source'][1] == power_station['power_station_id']:
return True
return False

View File

@@ -0,0 +1,148 @@
from math import ceil, floor
from helix.calculators.bom_helper import add_parts_to_list, apply_fudge_factors, \
get_panel_type_counts
from helix.calculators.subarray_helper import extract_subarray
from helix.constants.module_type import ModuleType
from helix.constants.panel_type import PanelType
from helix.constants.parts import link_tray, cross_tray, ballast, cross_tray_1_1, leading_tray
from helix.constants.system_type import SystemType
class MechanicalBomCalculator(object):
def __init__(self, values, panels, subarrays):
self.values = values
self.panels = panels
self.subarrays = subarrays
def mechanical_bom(self):
module_type = self.values.module_type()
system_type = self.values.system_type()
system_parts = system_type.parts(module_type)
combined_parts_list = {}
for subarray in self.subarrays:
panels = extract_subarray(self.panels, subarray.subarray_number)
ballast_count = sum(panel.ballast for panel in panels)
cross_count = sum(panel.cross_tray for panel in panels)
assigned_seismic_anchors_count = sum(panel.seismic_anchors for panel in panels)
required_seismic_anchors_count = subarray.required_seismic_anchors
seismic_anchors_count = max(assigned_seismic_anchors_count, required_seismic_anchors_count)
required_wind_anchors_count = sum(panel.wind_anchors for panel in panels)
anchor_count = required_wind_anchors_count + seismic_anchors_count
panel_type_counts = get_panel_type_counts(panels)
subarray_parts_list = {}
for index, panel_type_parts in enumerate(system_parts.parts_per_panel_type()):
add_parts_to_list(subarray_parts_list, panel_type_parts, panel_type_counts[PanelType.from_index(index)])
add_parts_to_list(subarray_parts_list, self.values.anchor_type().parts().parts, anchor_count)
link_count = self.link_count(panel_type_counts, panels, subarray)
add_parts_to_list(subarray_parts_list, {link_tray: 1}, link_count)
cross_tray_parts = cross_tray if self.values.module_type() == ModuleType.Cell96 else cross_tray_1_1
add_parts_to_list(subarray_parts_list, {cross_tray_parts: 1}, cross_count)
add_parts_to_list(subarray_parts_list, {ballast: 1}, ballast_count)
add_parts_to_list(subarray_parts_list, system_parts.row_parts(module_type), subarray.row_count)
add_parts_to_list(subarray_parts_list, system_parts.column_parts(module_type), subarray.column_count)
add_parts_to_list(subarray_parts_list, system_parts.sub_array_parts, 1)
apply_fudge_factors(subarray_parts_list, system_parts.fudge_factors(not subarray.row_counted_geometrically))
dependent_parts_list = {}
for part, quantity in subarray_parts_list.items():
dependent_parts = system_parts.dependent_parts(module_type).get(part)
if dependent_parts:
add_parts_to_list(dependent_parts_list, dependent_parts, quantity)
subarray_parts_list.update(dependent_parts_list)
add_parts_to_list(combined_parts_list, subarray_parts_list)
return combined_parts_list
def link_count(self, panel_type_counts, panels, subarray):
if self.values.system_type() == SystemType.dualTilt:
# check if info about position of panels is available
coordinates_available = all(p.coordinate for p in panels) \
and not all(p.coordinate.x == 0 and p.coordinate.y == 0 for p in panels)
if coordinates_available:
# initially every C, NS, EW panels has 2 link trays attached
panel_types = [PanelType.Corner, PanelType.NorthSouth, PanelType.EastWest]
layout = dict([((p.coordinate.x, p.coordinate.y), 2) for p in panels if p.panel_type in panel_types])
row_count = ceil(subarray.row_count)
column_count = ceil(subarray.column_count)
# reduce number of link trays between every two vertically adjoining panels
for y in range(row_count):
for x in range(column_count):
if (x, y) not in layout:
continue
if (x, y + 1) in layout:
layout[(x, y + 1)] = 1
# count link trays located on perimeter
link_count = sum([layout[p] for p in layout])
# subtract places reserved for leading trays
link_count -= subarray.row_count + 1
# add link trays for panels of type Middle
link_count += sum([1 for panel in panels if panel.link_tray != 0 and panel.panel_type == PanelType.Middle])
return max(link_count, 0)
else:
total_possible_link_trays = len(panels) + subarray.column_count
link_count = total_possible_link_trays
for panel in panels:
if panel.link_tray == 0 and panel.panel_type == PanelType.Middle:
link_count -= 1
link_count -= floor(subarray.row_count)
return link_count
else:
return sum([self.compute_link_count_single_tilt(panel_type, panel_type_counts, panels) for panel_type in PanelType.all()])
def compute_link_count_single_tilt(self, panel_type, panel_type_counts, panels):
if panel_type == PanelType.Corner:
return 0
elif panel_type == PanelType.NorthSouth:
return 0
elif panel_type == PanelType.EastWest:
return panel_type_counts[panel_type] * 2
elif panel_type == PanelType.Middle:
return self.get_panel_type_middle_link_trays_single_tilt(panels)
else:
return 0
def get_panel_type_middle_link_trays_single_tilt(self, panels):
wind_zones = self.values.system_type().system_constants().wind_zones
middle_panels_per_wind_zone = [0 for _ in wind_zones]
middle_link_trays_per_wind_zone = [0 for _ in wind_zones]
for panel in panels:
if panel.panel_type != PanelType.Middle:
continue
wind_zone = panel.wind_zone
middle_panels_per_wind_zone[wind_zone] += 1
middle_link_trays_per_wind_zone[wind_zone] += panel.link_tray # use calculated number of link trays to see if it is non-zero
total_link_trays_required = 0
for wind_zone, panel_count in enumerate(middle_panels_per_wind_zone):
if middle_link_trays_per_wind_zone[wind_zone] > 0: # if any middle panels in this wind zone need link trays
total_link_trays_required += ceil(panel_count * 1.05)
return total_link_trays_required

View File

@@ -0,0 +1,93 @@
from math import sqrt, log
import math
from helix.constants.global_constants import parapet_coefficients, parapet_factor_max
from numpy import array
from numpy.ma import maximum
from helix.constants.panel_type import PanelType
class PressureCoefficientCalculator(object):
def __init__(self, user_values):
self.values = user_values
self.system_constants = self.values.system_type().system_constants()
self.module_constants = self.values.module_system_constants()
def c_p_matrix(self, L_B):
parapet = self.parapet_factor()
return self.compute_c_p_matrix(L_B, parapet)
def L_B(self):
""" Building scaling factor """
height = max(15, self.values.building_height())
length = self.values.building_length()
width = self.values.building_width()
longest_side = max(width, length)
return min(height, 0.4 * sqrt(height * max(1, longest_side)))
def minimum_array_size(self, L_B):
panel_area = self.module_constants.panel_area
module_count = self.system_constants.module_count
minimum_array_size = []
for minimum_A_n in self.minimum_A_n(L_B):
if minimum_A_n is None:
value = 6
else:
value = int(math.ceil((minimum_A_n * L_B ** 2) / (panel_area * 1000) / module_count))
minimum_array_size.append(value)
return minimum_array_size
# Normalized area, scales the tributary area by the building scaling factor and panel area
def A_n(self, L_B):
return self.module_constants.tributary_area * (self.module_constants.panel_area * 1000. / L_B ** 2)
def compute_c_p_matrix(self, L_B, parapet):
A_n = self.A_n(L_B)
wind_zones = self.system_constants.wind_zones
return array([self.c_p_row(A_n, wind_zone, parapet) for wind_zone in wind_zones])
def c_p_row(self, A_n_row, wind_zone, parapet_factor):
c_p_lower_bound = self.module_constants.c_p_lower_bound()
if wind_zone == self.system_constants.wind_zones[-1]:
return c_p_lower_bound
computed_row = []
for index, A_n in enumerate(A_n_row):
edge_factor = self.module_constants.edge_factor(wind_zone, PanelType.from_index(index))
computed_row.append(self.c_p(A_n, wind_zone, parapet_factor, edge_factor))
return maximum(array(computed_row), c_p_lower_bound)
def c_p(self, A_n, wind_zone, parapet_factor, edge_factor):
c0, c1 = self.module_constants.c_p_constants(A_n, wind_zone)
return max(0., c0 * log(A_n) + c1) * parapet_factor * edge_factor
def parapet_factor(self):
height = max(15, self.values.building_height())
parapet_height = max(0, self.values.building_parapet_height())
factor = parapet_height / height
c0, c1 = parapet_coefficients
return min(parapet_factor_max, c0 + c1 * factor)
def ideal_subarray_average_uplift_c_p(self, L_B):
c_p_matrix = self.compute_c_p_matrix(L_B, 1)
return [self.module_constants.weighted_average_c_p(c, n, e, m) for c, n, e, m in c_p_matrix]
def minimum_A_n(self, L_B):
uplift_c_p = self.ideal_subarray_average_uplift_c_p(L_B)
minimum_A_n = []
for idx, wind_zone in enumerate(self.system_constants.wind_zones):
wind_zone_uplift = uplift_c_p[idx]
coefficients = self.module_constants.minimum_a_n_coefficients(wind_zone_uplift, wind_zone)
if not coefficients:
minimum_A_n.append(None)
continue
value = math.exp((coefficients[0] - wind_zone_uplift) / coefficients[1])
minimum_A_n.append(value)
return minimum_A_n

View File

@@ -0,0 +1,184 @@
from math import ceil, floor
from helix.calculators.subarray_graph import SubarrayGraph
from helix.calculators.subarray_helper import extract_subarray
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
from helix.models.subarray import Subarray
class SeismicCalculator(object):
def __init__(self, values, graph_repository):
self.values = values
self.system_type = values.system_type()
self.system_constants = values.module_system_constants()
self.anchor_type = values.anchor_type()
self.graph_repository = graph_repository
def assign_seismic_anchors(self, subarray, panels):
panel_data_for_subarray = extract_subarray(panels, subarray.subarray_number)
self.assign_anchors_to_subarray(panel_data_for_subarray, subarray)
return panels
def assign_anchors_to_subarray(self, panels, subarray):
sds = self.values.spectral_response()
wind_anchors = sum([panel.wind_anchors for panel in panels])
for panel in panels:
if panel.seismic_anchors is None:
panel.seismic_anchors = 0
required_anchors = subarray.required_seismic_anchors
if required_anchors == 0 and (wind_anchors == 0 or sds < 1):
return panels
graph = self.graph_repository.subarray_graph(subarray.subarray_number)
if len(graph.nodes) == 0:
return panels
more_anchors_needed = True
perimeter_covered = sds < 1.0
anchor_threshold = 0
while more_anchors_needed:
rung = graph.pop_rung()
interval = int(self.seismic_anchor_interval())
nodes_since_last_anchor = interval
if len(rung) == 0:
graph.reset()
anchor_threshold += 1
continue
while more_anchors_needed and interval >= 0:
for node in rung:
nodes_since_last_anchor += 1
if node.wind_anchor + node.seismic_anchor > anchor_threshold:
nodes_since_last_anchor = 0
if nodes_since_last_anchor > interval:
node.assign_seismic_anchor()
required_anchors -= 1
nodes_since_last_anchor = 0
more_anchors_needed = (not perimeter_covered) or required_anchors > 0
if not more_anchors_needed:
break
perimeter_covered = True
if interval <= 1:
interval -= 1
else:
interval /= 2
for idx, node in enumerate(graph.nodes):
panels[idx].seismic_anchors = node.seismic_anchor
return panels
def compute_provided_lateral_capacity(self, subarray_number, panels):
subarray_panels = extract_subarray(panels, subarray_number)
anchors = {PanelType.Corner: 0, PanelType.NorthSouth: 0, PanelType.EastWest: 0, PanelType.Middle: 0}
for panel in subarray_panels:
if panel.seismic_anchors is not None:
anchors[panel.panel_type] += panel.seismic_anchors
return self.anchors_shear_capacity(anchors[PanelType.Corner], anchors[PanelType.NorthSouth],
anchors[PanelType.EastWest], anchors[PanelType.Middle])
def seismic_anchors_for_subarray(self, F_p, subarray_weight, spectral_response, friction_coefficient,
seismic_anchors, corner_anchors,
north_south_anchors, east_west_anchors, middle_anchors, shear_capacity):
demand = self.seismic_demand_for_subarray(F_p, subarray_weight, spectral_response, friction_coefficient,
seismic_anchors, corner_anchors, north_south_anchors,
east_west_anchors, middle_anchors)
return ceil(demand / shear_capacity)
def anchors_shear_capacity(self, corner_anchors, north_south_anchors, east_west_anchors, middle_anchors):
anchor_shear_capacity = self.anchor_type.shear_capacity()
panel_racking_capacity = self.system_constants.racking_capacity
corner_capacity = corner_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.Corner))
north_south_capacity = north_south_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.NorthSouth))
east_west_capacity = east_west_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.EastWest))
middle_capacity = middle_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.Middle))
return corner_capacity + north_south_capacity + east_west_capacity + middle_capacity
def seismic_demand_for_subarray(self, F_p, subarray_weight, spectral_response, friction_coefficient,
seismic_anchors, corner_anchors,
north_south_anchors, east_west_anchors, middle_anchors):
if (corner_anchors + north_south_anchors + east_west_anchors + middle_anchors == 0) and seismic_anchors == 0:
return 0
existing_shear_resistance = self.anchors_shear_capacity(corner_anchors, north_south_anchors, east_west_anchors,
middle_anchors)
shear_force = 0.7 * F_p * subarray_weight - (
(0.6 - 0.14 * spectral_response) * 0.7 * friction_coefficient * subarray_weight) - existing_shear_resistance
return max(shear_force, 0)
def required_force_seismic_anchors(self, subarray_number, panels):
demand = self.required_force_seismic_demand(subarray_number, panels)
system_shear_capacity = min(self.anchor_type.shear_capacity(),
minimum_racking_capacity)
return ceil(demand / system_shear_capacity)
def required_force_seismic_demand(self, subarray_number, panels):
subarray_panels = extract_subarray(panels, subarray_number)
importance_factor = self.values.importance_factor()
spectral_response = self.values.spectral_response()
F_p = 1.2 * spectral_response / (1.5 / importance_factor)
# number of wind anchors by panel type
anchors = {PanelType.Corner: 0,
PanelType.NorthSouth: 0,
PanelType.EastWest: 0,
PanelType.Middle: 0}
# total weight
subarray_weight = 0
# total number of seismic anchors
seismic_anchors = 0
for panel in subarray_panels:
if panel.seismic_anchors is not None:
seismic_anchors += panel.seismic_anchors
# it could be calculated before the loop to avoid redundant calculations
effective_area = self.system_constants.surface_area / self.system_constants.ground_coverage_ratio
weight = panel.pressure * effective_area
anchors[panel.panel_type] += panel.wind_anchors
subarray_weight += weight
force_required_demand = self.seismic_demand_for_subarray(F_p, subarray_weight, spectral_response,
self.system_constants.friction_coefficient,
seismic_anchors,
anchors[PanelType.Corner],
anchors[PanelType.NorthSouth],
anchors[PanelType.EastWest],
anchors[PanelType.Middle])
return force_required_demand
def required_geometric_seismic_anchors(self, subarray_number, panels):
if panels[0].coordinate is None or self.values.spectral_response() < 1:
return 0
panel_data_for_subarray = extract_subarray(panels, subarray_number)
subarray = Subarray(required_seismic_anchors=0, subarray_number=subarray_number)
anchors_for_subarray = self.assign_anchors_to_subarray(panel_data_for_subarray, subarray)
return sum(panel.seismic_anchors for panel in anchors_for_subarray if panel.seismic_anchors)
def seismic_anchor_interval(self):
sds = self.values.spectral_response()
importance_factor = self.values.importance_factor()
interval_constant, interval_multiplier = self.system_constants.seismic_anchor_interval_constants
denom = (22.96 * importance_factor * sds - interval_constant + interval_multiplier * sds)
if denom <= 0:
return 15
return floor(minimum_racking_capacity / denom)

View File

@@ -0,0 +1,246 @@
import copy
from enum import Enum
from helix.constants.system_type import SystemType
from helix.models.coordinate import Coordinate
from helix.models.dxf.graph_node_store import GraphNodeStore
class Direction(Enum):
North = Coordinate(0, -1)
South = Coordinate(0, 1)
East = Coordinate(-1, 0)
West = Coordinate(1, 0)
def opposite_direction(self):
if self == Direction.North:
return Direction.South
elif self == Direction.South:
return Direction.North
elif self == Direction.East:
return Direction.West
else:
return Direction.East
@classmethod
def all(cls):
return [
cls.North,
cls.West,
cls.South,
cls.East,
]
@classmethod
def all_coordinates(cls):
return [d.value for d in cls.all()]
def directions_to_try(self):
return {
Direction.North: [Direction.East, Direction.North, Direction.West, Direction.South],
Direction.East: [Direction.South, Direction.East, Direction.North, Direction.West],
Direction.South: [Direction.West, Direction.South, Direction.East, Direction.North],
Direction.West: [Direction.North, Direction.West, Direction.South, Direction.East],
}[self]
class SubarrayGraphNode(object):
"""
A node that contains the panel, it's neighbors to the four cardinal
directions (N, S, E, W), and a
count of the seismic anchors attached.
"""
def __init__(self, panel):
self.neighbors = {}
self.panel = panel
self.seismic_anchor = 0
@property
def location(self):
return self.panel.coordinate
@property
def coordinate(self): # GraphNodeStore expects a coordinate property
return self.panel.coordinate
@property
def wind_anchor(self):
return self.panel.wind_anchors
def add_neighbor(self, neighbor, direction):
self.neighbors[direction] = neighbor
neighbor.neighbors[direction.opposite_direction()] = self
def remove_neighbor_references(self):
for direction, neighbor in self.neighbors.items():
neighbor.neighbors.pop(direction.opposite_direction(), None)
def assign_seismic_anchor(self):
self.seismic_anchor += 1
def step(self, direction):
return self.neighbors.get(direction)
def __repr__(self):
val = "(" + str(self.location) + ":\n"
for direction, neighbor in self.neighbors.items():
val += "\t%s: %s\n" % (str(direction), str(neighbor.location))
return val + ")"
def __eq__(self, other):
if self is other:
return True
us = self.panel.coordinate
them = other.panel.coordinate
# Quick and dirty, inline equality makes for a slightly faster equality check
# Also don't bother with floating point equality - it only slows us down. :(
return us.x == them.x and us.y == them.y and us.rotation == them.rotation
def __hash__(self):
return self.panel.coordinate.__hash__()
class SubarrayGraph(object):
def __init__(self, panels, system_type):
self.nodes = []
self.system_type = system_type
self.node_store = GraphNodeStore()
self.nodes = []
if len(panels) == 0 or panels[0].coordinate == panels[-1].coordinate:
self.nodes = []
else:
for panel in panels:
node = SubarrayGraphNode(panel)
self.nodes.append(node)
self.node_store.add_node(node)
self.graph = list(self.nodes)
self.rungs = []
self.current_rung = 0
self.assemble_graph()
def __deepcopy__(self, _):
panels = [node.panel for node in self.nodes]
graph = SubarrayGraph(panels, self.system_type)
return graph
def assemble_graph(self):
for node in self.nodes:
if len(node.neighbors) == 4:
continue
for direction in Direction.all():
coordinate = node.location + direction.value
neighbor = self.node_store.find_coordinate(coordinate)
if neighbor:
node.add_neighbor(neighbor, direction.opposite_direction())
if len(node.neighbors) == 4:
break
def reset(self):
self.graph = list(self.nodes)
self.current_rung = 0
def find_disconnected_subgraphs(self):
graph = list(self.graph)
subgraphs = []
while len(graph) > 0:
node = self.lower_left_node(graph)
subgraph = self.add_all_neighbors(node)
for subgraph_node in subgraph:
try:
graph.remove(subgraph_node)
except:
continue
subgraphs.append(subgraph)
return subgraphs
def find_node(self, coordinate):
if coordinate.x < 0 or coordinate.y < 0:
return None
for node in self.graph:
if node.location == coordinate:
return node
@staticmethod
def add_all_neighbors(node):
seen = {node}
visited = set()
visited_list = [] # apparently, order matters!
while len(seen) > 0:
node = seen.pop()
if node in visited:
continue
visited.add(node)
visited_list.append(node)
for neighbor in node.neighbors.values():
if neighbor not in visited and neighbor not in seen:
seen.add(neighbor)
return visited_list
@staticmethod
def lower_left_node(graph):
lower_left_node = None
lower_left_location = Coordinate(float('inf'), float('inf'))
for node in graph:
node_location = node.location
if node_location.x <= lower_left_location.x and node_location.y <= lower_left_location.y:
lower_left_node = node
lower_left_location = lower_left_node.location
return lower_left_node
def pop_rung(self):
if self.current_rung < len(self.rungs):
rung = self.rungs[self.current_rung]
self.current_rung += 1
return rung
rung = []
def assemble_rung_callback(node, next_direction, previous_direction):
rung.append(node)
if self.system_type == SystemType.dualTilt:
east_west = [Direction.East, Direction.West]
if next_direction in east_west or previous_direction in east_west:
rung.append(node)
subgraphs = self.find_disconnected_subgraphs()
for subgraph in subgraphs:
try:
start_node = self.lower_left_node(subgraph)
except:
break
self.walk_graph_perimeter(start_node, assemble_rung_callback)
for node in rung:
node.remove_neighbor_references()
try:
self.graph.remove(node)
except:
pass
self.rungs.append(rung)
self.current_rung += 1
return rung
def walk_graph_perimeter(self, start_node, fn, repeat_steps=True):
total_nodes = len(self.nodes)
steps = 0
node = start_node
direction = Direction.East
while True:
next_node = None
directions_to_try = list(direction.directions_to_try())
last_direction = direction
while next_node is None and len(directions_to_try) > 0:
direction = directions_to_try.pop(0)
next_node = node.step(direction)
if next_node is None:
break
fn(node, direction, last_direction)
node = next_node
steps += 1
if node == start_node or (steps > total_nodes and repeat_steps):
break

View File

@@ -0,0 +1,19 @@
from helix.models.subarray import Subarray
def get_subarray_sizes_and_rows(panels):
subarrays = []
last_subarray = None
for index, panel in enumerate(panels):
if last_subarray != panel.subarray:
subarray = Subarray(subarray_number=panel.subarray, start_row=index, size=0)
subarrays.append(subarray)
last_subarray = panel.subarray
subarrays[-1].size += 1
return subarrays
def extract_subarray(panels, subarray_number):
return [panel for panel in panels if panel.subarray == subarray_number]

View File

@@ -0,0 +1,85 @@
from collections import namedtuple
SummaryValues = namedtuple('SummaryValues', ['total_weight', 'max_psf', 'avg_psf', 'anchors', 'ballast', 'max_weight', 'ballast_weight'])
class SummaryValuesCalculator(object):
def __init__(self, user_values):
self.values = user_values
self.constants = self.values.module_system_constants()
def summary_values(self, panels, subarrays, c_p_matrix, q_z, seismic_interval, ballast_calculator):
summary_values = self.compute_summary(panels, subarrays, c_p_matrix, q_z, ballast_calculator)
return [
{'label': 'Total System Weight (lbs)', 'value': summary_values.total_weight},
{'label': 'Max PSF', 'value': summary_values.max_psf},
{'label': 'Avg PSF', 'value': summary_values.avg_psf},
{'label': 'Total Anchors', 'value': summary_values.anchors},
{'label': 'Total Ballast', 'value': summary_values.ballast},
{'label': 'Max Possible System Weight', 'value': summary_values.max_weight},
{'label': 'Max System Weight Ballast Block', 'value': summary_values.ballast_weight},
{'label': 'Seismic Anchor Max. Spacing', 'value': seismic_interval}
]
def documentation_summary_values(self, panels, subarrays, c_p_matrix, q_z, seismic_interval, ballast_calculator):
summary_values = self.compute_summary(panels, subarrays, c_p_matrix, q_z, ballast_calculator)
return {
'total_system_weight': summary_values.total_weight,
'max_psf': summary_values.max_psf,
'ave_psf': summary_values.avg_psf,
'total_anchors': summary_values.anchors,
'total_ballast': summary_values.ballast,
'max_possible_system_weight': summary_values.max_weight,
'max_system_weight_ballast_block': summary_values.ballast_weight,
'seismic_anchor_max_spacing': seismic_interval
}
def compute_summary(self, panels, subarrays, c_p_matrix, q_z, ballast_calculator):
total_weight, avg_psf = self.system_weight_and_pressure(panels)
max_psf = 0
wind_anchors = 0
total_ballast = 0
for panel in panels:
max_psf = panel.pressure if panel.pressure > max_psf else max_psf
wind_anchors += panel.wind_anchors
total_ballast += panel.ballast
required_seismic_anchors = sum(subarray.required_seismic_anchors for subarray in subarrays)
total_anchors = int(wind_anchors + required_seismic_anchors)
max_weight, ballast_weight = self.find_max_system_weight(panels, c_p_matrix, q_z, ballast_calculator)
return SummaryValues(
total_weight=round(total_weight),
max_psf=round(max_psf, 2),
avg_psf=round(avg_psf, 2),
anchors=total_anchors,
ballast=int(total_ballast),
max_weight=round(max_weight, 0),
ballast_weight=ballast_weight
)
def system_weight_and_pressure(self, panels):
constants = self.values.module_system_constants()
effective_area = constants.surface_area / constants.ground_coverage_ratio
psf_sum = sum(panel.pressure for panel in panels)
return psf_sum * effective_area, psf_sum / len(panels)
def find_max_system_weight(self, panels, c_p_matrix, q_z, ballast_calculator):
copied_panels = list(panels)
max_weight = 0
ballast_block_weight_for_max_weight = 0
for weight in range(12, 19):
ballast_matrix = ballast_calculator.ballast_and_trays_matrix(c_p_matrix, q_z, copied_panels,
ballast_block_weight=weight)
total_weight, _ = self.system_weight_and_pressure(ballast_matrix)
if total_weight > max_weight:
max_weight = total_weight
ballast_block_weight_for_max_weight = weight
return max_weight, ballast_block_weight_for_max_weight

View File

@@ -0,0 +1,56 @@
from math import log
from helix.constants.exposure_category import ExposureCategory
from helix.constants.global_constants import k_zt, k_d
class WindPressureCalculator(object):
def __init__(self, user_values):
self.values = user_values
def q_z(self, k_z):
v = self.values.wind_speed()
return 0.00256 * k_z * k_zt * k_d * v ** 2
def K_z(self):
height = self.values.building_height()
exposure = self.values.exposure_category()
transition_distance = self.values.exposure_category_transition_distance()
return self.calculate_k_z(height, exposure, transition_distance)
def calculate_k_z(self, h, exp, transition_distance):
if exp == ExposureCategory.B and h < 30:
return 0.7
elif exp == ExposureCategory.B_C or exp == ExposureCategory.C_B:
k_zd = self.k_zd(exp, h)
return k_zd + self.delta_k(exp, transition_distance, k_zd)
else:
return 2.01 * (self.h_eff(h, exp) / exp.z_g()) ** (2 / exp.alpha())
def h_eff(self, h, exposure):
if (exposure == ExposureCategory.C or exposure == ExposureCategory.D) and h < 15:
return 15.
else:
return h
def k_zd(self, exp, h):
if exp == ExposureCategory.B_C:
h_eff = max(15, h)
return self.calculate_expression(h_eff, exp.z_g()[1], exp.alpha()[1])
elif exp == ExposureCategory.C_B and h < 30:
return 0.7
else:
return self.calculate_expression(h, exp.z_g()[1], exp.alpha()[1])
def delta_k(self, exp, transition_distance, k_zd):
k_upwind = self.calculate_expression(33, exp.z_g()[0], exp.alpha()[0])
k_downwind = self.calculate_expression(33, exp.z_g()[1], exp.alpha()[1])
return (k_upwind - k_downwind) * (k_zd / k_downwind) * self.dk_x(k_upwind, k_downwind, transition_distance / 5280.)
def dk_x(self, k_upwind, k_downwind, transition_distance_in_miles):
x0 = 0.621 * 10 ** (-1 * ((k_upwind - k_downwind) ** 2) - 2.3)
x1 = 6.21 if k_upwind > k_downwind else 62.1
return log(x1 / transition_distance_in_miles, x1 / x0)
def calculate_expression(self, height, z_g, alpha):
return 2.01 * (height / z_g) ** (2 / alpha)

View File

View File

@@ -0,0 +1,23 @@
from helix.constants.parts import anchor_plate, anchor, anchor_washer
class OmgPowerGripParts(object):
parts = {
anchor_plate: 1,
anchor: 1,
anchor_washer: 1
}
class OmgPowerGripPlusParts(object):
parts = {
anchor_plate: 1,
anchor: 1,
anchor_washer: 1
}
class EcoFastenParts(object):
parts = {
anchor_plate: 1,
anchor: 1,
anchor_washer: 1
}

View File

@@ -0,0 +1,29 @@
from enum import Enum
from helix.constants.anchor_parts import OmgPowerGripParts, OmgPowerGripPlusParts, EcoFastenParts
from helix.constants.global_constants import system_force_capacity
class AnchorType(Enum):
OMG_PowerGrip = 'OMG PowerGrip'
OMG_PowerGrip_Plus = 'OMG PowerGrip Plus'
EcoFasten = 'EcoFasten Eco 65'
def uplift_capacity(self):
return min(system_force_capacity, {AnchorType.OMG_PowerGrip: 305.,
AnchorType.OMG_PowerGrip_Plus: 2000.,
AnchorType.EcoFasten: 1343.}[self])
def shear_capacity(self):
return {AnchorType.OMG_PowerGrip: 142.,
AnchorType.OMG_PowerGrip_Plus: 431.,
AnchorType.EcoFasten: 1008.}[self]
def parts(self):
return {AnchorType.OMG_PowerGrip: OmgPowerGripParts(),
AnchorType.OMG_PowerGrip_Plus: OmgPowerGripPlusParts(),
AnchorType.EcoFasten: EcoFastenParts()}[self]
@classmethod
def default_value(cls):
return cls.OMG_PowerGrip_Plus.value

View File

@@ -0,0 +1,4 @@
dt_chassis_fudge_factor = 1.04
leading_tray_fudge_factor = 1.05
bolts_per_package = 50

11
helix/constants/color.py Normal file
View File

@@ -0,0 +1,11 @@
from enum import Enum
class Color(Enum):
array_background = "white"
seismic_background = "#F1E8A2"
wind_background = "#B8F3E5"
default_panel_background = "#133256"
light_text = "white"
dark_text = "#6490BA"
border = "#537DAA"

View File

@@ -0,0 +1,89 @@
from helix.constants.module_type import ModuleType
from helix.constants.parts import *
class DualTiltParts(object):
east_west_panel_parts = {
dual_tilt_chassis: 1,
module: 2
}
center_panel_parts = {
dual_tilt_chassis: 1,
module: 2
}
sub_array_parts = {
leading_tray: 1
}
def __init__(self, module_type):
if module_type == ModuleType.PSeries:
self.corner_panel_parts = {
left_deflector_1_1: 1,
right_deflector_1_1: 1,
dual_tilt_chassis: 1.5,
module: 2
}
self.north_south_panel_parts = {
left_deflector_1_1: 1,
right_deflector_1_1: 1,
dual_tilt_chassis: 1.5,
module: 2
}
else:
self.corner_panel_parts = {
left_deflector: 1,
right_deflector: 1,
dual_tilt_chassis: 1.5,
module: 2
}
self.north_south_panel_parts = {
left_deflector: 1,
right_deflector: 1,
dual_tilt_chassis: 1.5,
module: 2
}
def row_parts(self, module_type):
if module_type == ModuleType.Cell96:
front_skirt_parts = front_skirt
else:
front_skirt_parts = front_skirt_1_1
return {
front_skirt_parts: 2,
leading_tray: 1
}
def column_parts(self, _):
return {}
def parts_per_panel_type(self):
return [
self.corner_panel_parts,
self.north_south_panel_parts,
self.east_west_panel_parts,
self.center_panel_parts
]
def dependent_parts(self, _):
return {
module: {
wire_clip: 2,
rubber_foot: 0.1
},
dual_tilt_chassis: {
dual_tilt_platform: 1,
platform_bolt: 4
}
}
def fudge_factors(self, used_fallback):
if used_fallback:
return {
dual_tilt_chassis: 1.04,
leading_tray: 1.05
}
return {
dual_tilt_chassis: 1.04,
}

View File

@@ -0,0 +1,7 @@
'''
Created on May 22, 2017
@author: jvazquez
'''
INVALID_DUAL_TILT_DESIGN = "Error - not a dual tilt file, or invalid "\
"dual tilt design."

View File

@@ -0,0 +1,281 @@
from helix.constants.inverter_type import InverterType
from helix.constants.module_type import ModuleType
from helix.constants.parts import *
from helix.constants.system_type import SystemType
inverter_model_parts = {
InverterType.SMA.MODEL_12KW: {sma_12kw_inverter: 1},
InverterType.SMA.MODEL_15KW: {sma_15kw_inverter: 1},
InverterType.SMA.MODEL_20KW: {sma_20kw_inverter: 1},
InverterType.SMA.MODEL_24KW: {sma_24kw_inverter: 1},
InverterType.DELTA.MODEL_36KW: {delta_36kw_inverter: 1},
InverterType.DELTA.MODEL_42KW: {delta_42kw_inverter: 1},
InverterType.DELTA.MODEL_60KW: {delta_60kw_inverter: 1},
InverterType.DELTA.MODEL_80KW: {delta_80kw_inverter: 1},
}
inverter_strings_parts = {
0: {},
2: {
harness_2_string_mf: 1,
harness_2_string_fm: 1
},
3: {
harness_3_string_mf: 1,
harness_3_string_fm: 1
},
4: {
harness_2_string_mf: 2,
harness_2_string_fm: 2
},
5: {
harness_2_string_mf: 1,
harness_2_string_fm: 1,
harness_3_string_mf: 1,
harness_3_string_fm: 1
},
6: {
harness_3_string_mf: 2,
harness_3_string_fm: 2
},
7: {
harness_3_string_mf: 1,
harness_3_string_fm: 1,
harness_4_string_mf: 1,
harness_4_string_fm: 1
},
8: {
harness_4_string_mf: 2,
harness_4_string_fm: 2
},
}
def shared_panel_board_parts(module_type, system_type):
v1_1_inverter_links = 0
if system_type == SystemType.singleTilt and (module_type == ModuleType.PSeries or module_type == ModuleType.Cell128):
v1_1_inverter_links = 1
return {
front_legs: 1,
back_legs: 1,
inverter_link: 2,
inverter_rail: 1,
rubber_foot: 3,
mounting_back_plate: 1,
ethernet_plug: 1.8,
flat_washer: 4,
hex_nut_three_eighths_16: 2,
hex_bolt_1_2: 9,
inverter_link_long: v1_1_inverter_links,
inverter_link_short: v1_1_inverter_links,
}
dc_switch_parts = {
dc_switch_bracket: 1,
dc_switch_box: 1,
hex_bolt_3_4: 4,
hex_bolt_quarter_20: 4,
hex_nut_three_eighths_16: 4,
hex_nut_quarter_20: 4,
flat_washer_quarter_inch: 4,
}
def panel_board_parts(inverter_quantity, with_aux):
if with_aux:
parts = {
1: {
panel_board_2_aux: 1,
},
2: {
panel_board_2_aux: 1,
},
3: {
panel_board_3_aux: 1,
},
4: {
panel_board_4_aux: 1,
}
}
else:
parts = {
1: {
panel_board_2: 1,
},
2: {
panel_board_2: 1,
},
3: {
panel_board_3: 1,
},
4: {
panel_board_4: 1,
}
}
return parts[inverter_quantity]
def panel_board_parts_with_monitor(inverter_quantity, monitor_controller_type):
parts = panel_board_parts(inverter_quantity, monitor_controller_type != monitor_controller_240_v)
parts[monitor_controller_type] = 1
return parts
def standalone_inverter_parts(inverter, system_type, module_type):
multiplier = 1
v1_1_inverter_links = 0
if inverter['model'] in InverterType.DELTA.all():
parts = {}
if system_type == SystemType.singleTilt:
parts = {**parts, delta_kit_inverter_mount: 1}
else:
parts = {**parts, delta_kit_inverter_mount_dt: 1}
if inverter['splice_box']:
parts = {**parts, delta_splice_box: 1}
return parts
if system_type == SystemType.singleTilt:
multiplier = 2
v1_1_inverter_links = 1 if module_type == ModuleType.PSeries or module_type == ModuleType.Cell128 else 0
return {
ac_switch: 1,
star_washer: 4,
phillips_screw: 4 * multiplier,
ac_inverter_bracket: 1 * multiplier,
hex_bolt_1_2: 4,
flat_washer_6: 4,
inverter_link_short: v1_1_inverter_links,
inverter_link_long: v1_1_inverter_links,
whip_tray: 1
}
standalone_inverter_attached_to_panel_board_parts = {
ac_splice_box: 1,
}
def inverter_parts(inverter, module_type):
if inverter['model'] in InverterType.DELTA.all():
return {
delta_inverter_leg: 3,
rubber_foot: 3,
}
else:
if module_type == ModuleType.PSeries:
return {
flat_washer: 8,
hex_nut_three_eighths_16: 6,
hex_bolt_3_4: 5,
hex_bolt_1_2: 18,
front_legs: 1,
back_legs: 1,
mounting_back_plate: 1,
rubber_foot: 3,
inverter_link: 2,
inverter_rail: 1,
stump: 6,
fuseshade: 1,
screw_12_24x1_25: 2,
fuseshade_brace: 1,
}
else:
return {
flat_washer: 4,
hex_nut_three_eighths_16: 4,
hex_bolt_3_4: 2,
hex_bolt_1_2: 18,
front_legs: 1,
back_legs: 1,
mounting_back_plate: 1,
rubber_foot: 3,
inverter_link: 2,
inverter_rail: 1,
stump: 6,
}
def dependent_parts(module_type, system_type):
v1_1_inverter_links = 0
if system_type == SystemType.singleTilt and (module_type == ModuleType.Cell128 or module_type == ModuleType.PSeries):
v1_1_inverter_links = 1
return {
panel_board_2: {
harness_ac_inner: 2,
whip_tray: 2,
comm_cable: 1
},
panel_board_2_aux: {
harness_ac_inner: 2,
whip_tray: 2,
comm_cable: 2
},
panel_board_3: {
harness_ac_inner: 2,
harness_ac_outer: 1,
whip_tray: 3,
comm_cable: 2,
inverter_link_short: v1_1_inverter_links,
inverter_link_long: v1_1_inverter_links,
inverter_link: -2 * v1_1_inverter_links,
},
panel_board_3_aux: {
harness_ac_inner: 2,
harness_ac_outer: 1,
whip_tray: 3,
comm_cable: 3,
inverter_link_short: v1_1_inverter_links,
inverter_link_long: v1_1_inverter_links,
inverter_link: -2 * v1_1_inverter_links,
},
panel_board_4: {
harness_ac_inner: 2,
harness_ac_outer: 2,
whip_tray: 4,
comm_cable: 3,
inverter_link_short: v1_1_inverter_links,
inverter_link_long: v1_1_inverter_links,
inverter_link: -2 * v1_1_inverter_links,
},
panel_board_4_aux: {
harness_ac_inner: 2,
harness_ac_outer: 2,
whip_tray: 4,
comm_cable: 4,
inverter_link_short: v1_1_inverter_links,
inverter_link_long: v1_1_inverter_links,
inverter_link: -2 * v1_1_inverter_links,
},
monitor_controller_480_v: {
monitor_power_plug: 1,
flat_washer: 4,
channel_nut: 4,
hex_nut_three_eighths_16: 2,
front_legs: 1,
back_legs: 1,
inverter_link: 2,
inverter_rail: 1,
rubber_foot: 3,
hex_bolt_1_2: 9,
mounting_back_plate: 1,
},
monitor_controller_240_v: {},
ac_splice_box: {
hex_bolt_1_2: 4,
hex_bolt_3_4: 4,
hex_nut_three_eighths_16: 8,
flat_washer: 8,
back_legs: 1,
front_legs: 1,
rubber_foot: 3,
inverter_rail: 1,
inverter_link: 2,
mounting_back_plate: 1,
},
inverter_link_long: {
inverter_link: -1,
},
inverter_link_short: {
inverter_link: -1,
}
}

View File

@@ -0,0 +1,34 @@
from enum import Enum
class ExposureCategory(Enum):
B = 'B'
C = 'C'
D = 'D'
B_C = 'B to C'
C_B = 'C to B'
def z_g(self):
return {
ExposureCategory.B: 1200.,
ExposureCategory.C: 900.,
ExposureCategory.D: 700.,
ExposureCategory.B_C: (1273., 906.),
ExposureCategory.C_B: (906., 1273.)
}[self]
def alpha(self):
return {
ExposureCategory.B: 7.,
ExposureCategory.C: 9.5,
ExposureCategory.D: 11.5,
ExposureCategory.B_C: (6.62, 9.5),
ExposureCategory.C_B: (9.5, 6.62)
}[self]
def is_interpolated(self):
return self in [ExposureCategory.B_C, ExposureCategory.C_B]
@classmethod
def default_value(cls):
return cls.C.value

View File

@@ -0,0 +1,50 @@
from enum import Enum
from helix.constants.system_type import SystemType
class FileValidationMessage(Enum):
Generic = 'Input file has invalid data'
InvalidHeaders = 'Input file has less than 5 headers'
InvalidRowCount = 'Input file has no data'
DualTiltWindZone = 'Invalid wind zone for Dual Tilt System'
SingleTiltWindZone = 'Invalid wind zone for Single Tilt System'
PanelTypeOutOfBounds = 'Invalid Panel Type (Not in 1-4)'
PanelTypeTooFewCornersSingleTilt = 'Input file must have at least 4 corner modules per subarray'
PanelTypeTooFewCornersDualTilt = 'Input file must have at least 2 corner modules per subarray'
UnknownFileUploaded = 'Please upload a valid .txt or .dxf file'
ExpectedTxtFile = 'Invalid file uploaded. Please upload a valid .txt file.'
ExpectedDxfFile = 'Invalid file uploaded. Please upload a valid .dxf file.'
OldDxfFormat = 'Invalid Aurora format uploaded. Please upload a new format.'
@classmethod
def invalid_wind_zones(cls, system_type):
if system_type == SystemType.singleTilt:
return cls.SingleTiltWindZone
else:
return cls.DualTiltWindZone
@classmethod
def panel_type_too_few_corners(cls, system_type):
return {
SystemType.singleTilt: cls.PanelTypeTooFewCornersSingleTilt,
SystemType.dualTilt: cls.PanelTypeTooFewCornersDualTilt,
}[system_type]
class FileValidationError(object):
def __init__(self, validation_message, row_number):
self.row_number = row_number
self.validation_message = validation_message
def format_error_message(self):
if self.row_number:
return self.validation_message.value + ' on line ' + str(self.row_number)
return self.validation_message.value
def __repr__(self):
return "<ValidationError: " + self.format_error_message() + ">"
def __eq__(self, other):
if self.__class__ != other.__class__:
return False
return self.row_number == other.row_number and self.validation_message == other.validation_message

View File

@@ -0,0 +1,9 @@
k_d = 0.85
k_zt = 1.
parapet_factor_max = 1.12
parapet_coefficients = 0.88, 1.2
system_force_capacity = 418.
minimum_racking_capacity = 226

View File

@@ -0,0 +1,25 @@
from enum import IntEnum
class InverterBrand(IntEnum):
SMA = 1
DELTA = 2
@property
def label(self):
return {
self.SMA: 'SMA',
self.DELTA: 'Delta',
}[self]
@classmethod
def default_value(cls):
return cls.SMA.value
@classmethod
def all(cls):
return [cls.SMA, cls.DELTA]
@classmethod
def dict(cls):
return {cls.SMA.label: cls.SMA.value, cls.DELTA.label: cls.DELTA.value}

View File

@@ -0,0 +1,94 @@
from enum import IntEnum
class InverterTypeSMA(IntEnum):
MODEL_12KW = 4
MODEL_15KW = 5
MODEL_20KW = 6
MODEL_24KW = 8
@property
def default_string(self):
return {
self.MODEL_12KW: 4,
self.MODEL_15KW: 5,
self.MODEL_20KW: 6,
self.MODEL_24KW: 8,
}[self]
@property
def label(self):
return {
self.MODEL_12KW: '12kW SMA Tripower - 514686',
self.MODEL_15KW: '15kW SMA Tripower - 514687',
self.MODEL_20KW: '20kW SMA Tripower - 512676',
self.MODEL_24KW: '24kW SMA Tripower - 514685',
}[self]
@property
def valid_string_ranges(self):
return {
self.MODEL_12KW: range(2, 9),
self.MODEL_15KW: range(2, 9),
self.MODEL_20KW: range(2, 9),
self.MODEL_24KW: range(2, 9),
}[self]
@classmethod
def default_value(cls):
return cls.MODEL_24KW.value
@classmethod
def all(cls):
return [cls.MODEL_12KW, cls.MODEL_15KW, cls.MODEL_20KW, cls.MODEL_24KW]
class InverterTypeDelta(IntEnum):
MODEL_36KW = 9
MODEL_42KW = 10
MODEL_60KW = 11
MODEL_80KW = 12
@property
def label(self):
return {
self.MODEL_36KW: '36kW Delta - 524952',
self.MODEL_42KW: '42kW Delta - 524969',
self.MODEL_60KW: '60kW Delta - 524954',
self.MODEL_80KW: '80kW Delta - 524955',
}[self]
@property
def default_string(self):
return {
self.MODEL_36KW: None,
self.MODEL_42KW: None,
self.MODEL_60KW: None,
self.MODEL_80KW: 14,
}[self]
@property
def valid_string_ranges(self):
return {
self.MODEL_36KW: None,
self.MODEL_42KW: None,
self.MODEL_60KW: None,
self.MODEL_80KW: range(14, 25),
}[self]
@classmethod
def default_value(cls):
return cls.MODEL_60KW.value
@classmethod
def all(cls):
return [cls.MODEL_36KW, cls.MODEL_42KW, cls.MODEL_60KW, cls.MODEL_80KW]
class InverterType:
SMA = InverterTypeSMA
DELTA = InverterTypeDelta
@classmethod
def all(cls):
return cls.SMA.all() + cls.DELTA.all()

View File

@@ -0,0 +1,11 @@
from enum import Enum
class ModuleType(Enum):
Cell96 = '96 Cell'
Cell128 = '128 Cell'
PSeries = 'P-Series'
@classmethod
def default_value(cls):
return cls.Cell96.value

View File

@@ -0,0 +1,113 @@
from numpy import array
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
class DualTilt128CellConstants(object):
panel_spacing = (88.24, 82.0) # inches
spacing_size_inches = 5.12 # This is inches
presenter_spacing = (1.5, 1.5)
panel_area = 23.29
surface_area = 46.58 # aka tent area
tributary_area = array([3.23, 6.84, 6.54, 13.57])
ground_coverage_ratio = 0.91
friction_coefficient = 0.59
seismic_anchor_interval_constants = (13.8768, 3.23792)
max_psf = 32
def c_p_lower_bound(self):
return array([0.06, 0.05, 0.052, 0.039])
def edge_factor(self, wind_zone, panel_type):
return 1
def racking_capacity(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return minimum_racking_capacity
else:
return 308
def c_p_constants(self, A_n, wind_zone):
if wind_zone == 'A':
if A_n < 825:
return -0.144, 1.1451
else:
return -0.058, 0.5651
elif wind_zone == 'B':
if A_n < 465:
return -0.117, 0.8824
elif A_n < 825:
return -0.087, 0.6981
else:
return -0.033, 0.3336
elif wind_zone == 'C':
if A_n < 465:
return -0.073, 0.5479
elif A_n < 825:
return -0.035, 0.3143
else:
return -0.015, 0.184
elif wind_zone == 'D':
if A_n < 465:
return -0.039, 0.303
elif A_n < 825:
return -0.023, 0.2023
else:
return -0.008, 0.102
else:
return 1, 1
def base_weight(self, panel_type, tray_count):
if panel_type == PanelType.Corner:
return [108.66, 110.96, 112.11, 116.44, 119.62, 122.80, 125.98][tray_count]
elif panel_type == PanelType.NorthSouth:
return [107.58, 109.88, 111.03, 114.21, 117.39, 120.57, 123.75][tray_count]
elif panel_type == PanelType.EastWest:
return [103.19, 105.49, 105.49, 108.67, 111.85, 115.03, 118.21][tray_count]
else:
return [102.11, 104.41, 104.41, 107.59, 110.77, 113.95, 117.13][tray_count]
def link_tray_thresholds(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [7.5, 10, 15]
else:
return [5, 7.5, 10]
def cross_tray_thresholds(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [15, 23, 31, 39, 47]
else:
return [10, 18, 26, 34, 42]
def weighted_average_c_p(self, corner, north_south, east_west, middle):
return (corner * 2. + east_west * 26. + middle * 104. + north_south * 8.) / 140.
def minimum_a_n_coefficients(self, uplift_c_p, wind_zone):
if wind_zone == 'A':
if uplift_c_p > 0.275:
return 1.5305, 0.187
else:
return 0.9252, 0.097
elif wind_zone == 'B':
if uplift_c_p > 0.2292:
return 1.2058, 0.159
elif uplift_c_p > 0.1534:
return 1.0334, 0.131
else:
return 0.4411, 0.043
elif wind_zone == 'C':
if uplift_c_p > 0.151:
return 0.7899, 0.104
elif uplift_c_p > 0.108:
return 0.5785, 0.07
else:
return 0.2921, 0.027
elif wind_zone == 'D':
if uplift_c_p > 0.0929:
return 0.3939, 0.049
elif uplift_c_p > 0.069:
return 0.3043, 0.035
else:
return 0.122, 0.008
return None

View File

@@ -0,0 +1,121 @@
from numpy import array
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
class DualTilt96CellConstants(object):
panel_spacing = (88.24, 62.0) # inches
spacing_size_inches = 5.12 # This is inches
presenter_spacing = (1.5, 1)
panel_area = 17.58
surface_area = 35.14 # aka tent area
tributary_area = array([3.32, 6.58, 7.29, 21.74]) # for each panel type
ground_coverage_ratio = 0.91
friction_coefficient = 0.59
seismic_anchor_interval_constants = (10.1598, 2.3706)
max_psf = 48
def c_p_lower_bound(self):
return array([0.06, 0.05, 0.052, 0.039])
def edge_factor(self, wind_zone, panel_type):
return 1
def racking_capacity(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return minimum_racking_capacity
else:
return 308
def c_p_constants(self, A_n, wind_zone):
if wind_zone == 'A':
if A_n < 825:
return -0.144, 1.1451
else:
return -0.058, 0.5651
elif wind_zone == 'B':
if A_n < 465:
return -0.117, 0.8824
elif A_n < 825:
return -0.087, 0.6981
else:
return -0.033, 0.3336
elif wind_zone == 'C':
if A_n < 465:
return -0.073, 0.5479
elif A_n < 825:
return -0.035, 0.3143
else:
return -0.015, 0.184
elif wind_zone == 'D':
if A_n < 465:
return -0.039, 0.303
elif A_n < 825:
return -0.023, 0.2023
else:
return -0.008, 0.102
else:
return 1, 1
def base_weight(self, panel_type, tray_count):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [92.58,
94.31,
96.03,
98.33,
100.63,
102.93,
105.23][tray_count]
else:
return [87.11,
88.84,
89.41,
91.71,
94.01,
96.31,
98.61][tray_count]
def link_tray_thresholds(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [7.5, 10, 15]
else:
return [5, 7.5, 10]
def cross_tray_thresholds(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [15, 24, 33, 42, 51]
else:
return [10, 19, 28, 37, 46]
def weighted_average_c_p(self, corner, north_south, east_west, middle):
return (corner * 2. + east_west * 26. + middle * 104. + north_south * 8.) / 140.
def minimum_a_n_coefficients(self, uplift_c_p, wind_zone):
if wind_zone == 'A':
if uplift_c_p > 0.275:
return 1.5305, 0.187
else:
return 0.9252, 0.097
elif wind_zone == 'B':
if uplift_c_p > 0.2292:
return 1.2058, 0.159
elif uplift_c_p > 0.1537:
return 1.0334, 0.131
else:
return 0.4411, 0.043
elif wind_zone == 'C':
if uplift_c_p > 0.151:
return 0.7899, 0.104
elif uplift_c_p > 0.108:
return 0.5785, 0.07
else:
return 0.2921, 0.027
elif wind_zone == 'D':
if uplift_c_p > 0.093:
return 0.3939, 0.049
elif uplift_c_p > 0.069:
return 0.3043, 0.035
else:
return 0.122, 0.008
return None

View File

@@ -0,0 +1,113 @@
from numpy import array
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
class DualTiltPSeriesConstants(object):
panel_spacing = (88.24, 82.0) # inches
spacing_size_inches = 8.8503937 # This is inches
presenter_spacing = (1.5, 1.5)
panel_area = 22.22
surface_area = 44.44 # aka tent area
tributary_area = array([3.23, 6.84, 6.54, 13.57])
ground_coverage_ratio = 0.91
friction_coefficient = 0.59
seismic_anchor_interval_constants = (12.6378, 2.94882)
max_psf = 32
def c_p_lower_bound(self):
return array([0.06, 0.05, 0.052, 0.039])
def edge_factor(self, wind_zone, panel_type):
return 1
def racking_capacity(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return minimum_racking_capacity
else:
return 308
def c_p_constants(self, A_n, wind_zone):
if wind_zone == 'A':
if A_n < 825:
return -0.144, 1.1451
else:
return -0.058, 0.5651
elif wind_zone == 'B':
if A_n < 465:
return -0.117, 0.8824
elif A_n < 825:
return -0.087, 0.6981
else:
return -0.033, 0.3336
elif wind_zone == 'C':
if A_n < 465:
return -0.073, 0.5479
elif A_n < 825:
return -0.035, 0.3143
else:
return -0.015, 0.184
elif wind_zone == 'D':
if A_n < 465:
return -0.039, 0.303
elif A_n < 825:
return -0.023, 0.2023
else:
return -0.008, 0.102
else:
return 1, 1
def base_weight(self, panel_type, tray_count):
if panel_type == PanelType.Corner:
return [103.66, 105.96, 107.11, 111.44, 114.62, 117.80, 120.98][tray_count]
elif panel_type == PanelType.NorthSouth:
return [102.58, 104.88, 106.03, 109.21, 112.39, 115.57, 118.75][tray_count]
elif panel_type == PanelType.EastWest:
return [98.19, 100.49, 100.49, 103.67, 106.85, 110.03, 113.21][tray_count]
else:
return [97.11, 99.41, 99.41, 102.59, 105.77, 108.95, 112.13][tray_count]
def link_tray_thresholds(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [7.5, 10, 15]
else:
return [5, 7.5, 10]
def cross_tray_thresholds(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
return [15, 23, 31, 39, 47]
else:
return [10, 18, 26, 34, 42]
def weighted_average_c_p(self, corner, north_south, east_west, middle):
return (corner * 2. + east_west * 26. + middle * 104. + north_south * 8.) / 140.
def minimum_a_n_coefficients(self, uplift_c_p, wind_zone):
if wind_zone == 'A':
if uplift_c_p > 0.275:
return 1.5305, 0.187
else:
return 0.9252, 0.097
elif wind_zone == 'B':
if uplift_c_p > 0.2292:
return 1.2058, 0.159
elif uplift_c_p > 0.1537:
return 1.0334, 0.131
else:
return 0.4411, 0.043
elif wind_zone == 'C':
if uplift_c_p > 0.151:
return 0.7899, 0.104
elif uplift_c_p > 0.108:
return 0.5785, 0.07
else:
return 0.2921, 0.027
elif wind_zone == 'D':
if uplift_c_p > 0.093:
return 0.3939, 0.049
elif uplift_c_p > 0.069:
return 0.3043, 0.035
else:
return 0.122, 0.008
return None

View File

@@ -0,0 +1,236 @@
from numpy import array
from numpy.ma import log
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
from helix.constants.system_type_constants.single_tilt_constants import SingleTiltConstants
class SingleTilt128CellConstants(object):
panel_spacing = (82.0, 60.0) # inches
presenter_spacing = (1.5, 1)
panel_area = 23.29
surface_area = 23.29
tributary_area = array([1.87, 3.24, 3.24, 3.82])
ground_coverage_ratio = 0.67
friction_coefficient = 0.42
seismic_anchor_interval_constants = (9.8784, 2.30496)
system_constants = SingleTiltConstants()
max_psf = 32
def c_p_lower_bound(self):
c_p_lower_bound = []
for index, area in enumerate(self.tributary_area):
if area < 9:
c_p = -0.0273 * log(area) + 0.1401
else:
c_p = -0.0146 * log(area) + 0.112
edge_factor = self.edge_factor(self.system_constants.wind_zones[-1], PanelType.from_index(index))
c_p_lower_bound.append(c_p * edge_factor)
return array(c_p_lower_bound)
def edge_factor(self, wind_zone, panel_type):
# corner, north/south, east/west, middle
edge_matrix = {'A': [1.2, 1.2, 1.0, 1.0],
'B': [1.2, 1.2, 1.0, 1.0],
'C': [1.4, 1.4, 1.0, 1.0],
'D': [1.4, 1.4, 1.0, 1.0],
'E': [1.5, 1.0, 1.5, 1.0],
'F': [1.3, 1.3, 1.0, 1.0],
'G': [1.0, 1.0, 1.0, 1.0],
'H': [1.0, 1.0, 1.0, 1.0],
'I': [1.0, 1.0, 1.0, 1.0],
'J': [2.0, 2.0, 1.0, 1.0],
'K': [1.4, 1.4, 1.0, 1.0]}
return edge_matrix[wind_zone][panel_type.index()]
def racking_capacity(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.EastWest:
return minimum_racking_capacity
else:
return 308
def c_p_constants(self, A_n, wind_zone):
if wind_zone == 'A':
if A_n < 421.5:
return -0.2245, 1.717
elif A_n < 2294.7:
return -0.1298, 1.1446
else:
return -0.0308, 0.3787
elif wind_zone == 'B':
if A_n < 421.5:
return -0.1818, 1.3585
elif A_n < 2294.7:
return -0.0708, 0.688
else:
return -0.0308, 0.3787
elif wind_zone == 'C':
if A_n < 421.5:
return -0.0735, 0.6443
elif A_n < 2294.7:
return -0.059, 0.5566
else:
return -0.021, 0.2627
elif wind_zone == 'D':
if A_n < 187.3:
return -0.1007, 0.6868
elif A_n < 421.5:
return -0.049, 0.4181
else:
return -0.0183, 0.2304
elif wind_zone == 'E':
if A_n < 187.3:
return -0.058, 0.4236
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
elif wind_zone == 'F':
if A_n < 187.3:
return -0.0655, 0.4682
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
elif wind_zone == 'G':
if A_n < 187.3:
return -0.0341, 0.2786
elif A_n < 421.5:
return -0.0271, 0.242
else:
return -0.0125, 0.1533
elif wind_zone == 'H':
if A_n < 187.3:
return -0.1161, 0.7872
elif A_n < 421.5:
return -0.074, 0.5672
elif A_n < 1685.9:
return -0.0433, 0.3816
else:
return -0.0117, 0.1473
elif wind_zone == 'I':
if A_n < 187.3:
return -0.3856, 2.258
elif A_n < 421.5:
return -0.148, 1.0143
elif A_n < 1685.9:
return -0.0433, 0.3816
else:
return -0.0117, 0.1473
elif wind_zone == 'J':
if A_n < 187.3:
return -0.1024, 0.6557
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
else:
return 1, 1
def base_weight(self, panel_type, tray_count):
if panel_type == PanelType.Corner:
return [71.91, 71.91, 75.09, 78.27][tray_count]
elif panel_type == PanelType.NorthSouth:
return [65.8, 65.8, 68.98, 72.16][tray_count]
elif panel_type == PanelType.EastWest:
return [69.75, 72.05, 75.23, 78.41][tray_count]
else:
return [65.08, 67.38, 70.56, 73.74][tray_count]
def link_tray_thresholds(self, panel_type):
return [[0, 13.0],
[0, 10.00],
[7, 14.5],
[6, 11.0]][panel_type.index()]
def cross_tray_thresholds(self, panel_type):
return [[13.0, 21.0, 29.0],
[10.00, 18.0, 26.0],
[14.5, 22.5, 30.5],
[11.0, 19.0, 27.0]][panel_type.index()]
def weighted_average_c_p(self, corner, north_south, east_west, middle):
""" Based on a 30 by 28 rectangular array """
return (middle * 182. + north_south * 14. + east_west * 13. + corner) / 210.
def minimum_a_n_coefficients(self, uplift_c_p, wind_zone):
if wind_zone == 'A':
if uplift_c_p > 0.360:
return 3.1178, 0.3769
elif uplift_c_p > 0.180:
return 2.4967, 0.274
else:
return 0.9691, 0.0685
elif wind_zone == 'B':
if uplift_c_p > 0.260:
return 2.3762, 0.2807
elif uplift_c_p > 0.160:
return 1.7699, 0.1803
else:
return 0.7209, 0.0392
elif wind_zone == 'C':
if uplift_c_p > 0.200:
return 1.7251, 0.215
elif uplift_c_p > 0.100:
return 1.2668, 0.1274
else:
return 0.4655, 0.0196
elif wind_zone == 'D':
if uplift_c_p > 0.120:
return 2.3762, 0.2807
elif uplift_c_p > 0.094:
return 1.7699, 0.1803
else:
return 0.7209, 0.0392
elif wind_zone == 'E':
if uplift_c_p > 0.078:
return 0.8096, 0.0976
elif uplift_c_p > 0.060:
return 0.438, 0.0361
else:
return 0.3227, 0.0206
elif wind_zone == 'F':
if uplift_c_p > 0.078:
return 0.6281, 0.0708
else:
return 0.328, 0.0212
elif wind_zone == 'G':
if uplift_c_p > 0.078:
return 0.6281, 0.0708
else:
return 0.328, 0.0212
elif wind_zone == 'H':
if uplift_c_p > 0.180:
return 1.8215, 0.2525
elif uplift_c_p > 0.120:
return 1.4679, 0.185
elif uplift_c_p > 0.060:
return 1.13479, 0.1298
else:
return 0.3227, 0.0206
elif wind_zone == 'I':
if uplift_c_p > 0.240:
return 4.3609, 0.6996
elif uplift_c_p > 0.120:
return 2.639, 0.3699
elif uplift_c_p > 0.060:
return 1.4027, 0.1659
else:
return 0.3277, 0.0206
elif wind_zone == 'J':
if uplift_c_p > 0.120:
return 0.7131, 0.0891
elif uplift_c_p > 0.078:
return 0.5503, 0.058
else:
return 0.328, 0.0212
elif wind_zone == 'K':
if uplift_c_p > 0.080:
return 0.3, 0.0455
else:
return 0.2465, 0.0212
else:
return 1, 1

View File

@@ -0,0 +1,231 @@
from numpy import array
from numpy.ma import log
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
from helix.constants.system_type_constants.single_tilt_constants import SingleTiltConstants
class SingleTilt96CellConstants(object):
panel_spacing = (62.0, 60.0) # inches
presenter_spacing = (1, 1)
panel_area = 17.58
surface_area = 17.57
tributary_area = array([2.07, 4.06, 4.06, 9.79])
ground_coverage_ratio = 0.67
friction_coefficient = 0.42
seismic_anchor_interval_constants = (7.2324, 1.6876)
system_constants = SingleTiltConstants()
max_psf = 48
def c_p_lower_bound(self):
c_p_lower_bound = []
for index, area in enumerate(self.tributary_area):
if area < 9:
c_p = -0.0273 * log(area) + 0.1401
else:
c_p = -0.0146 * log(area) + 0.112
edge_factor = self.edge_factor(self.system_constants.wind_zones[-1], PanelType.from_index(index))
c_p_lower_bound.append(c_p * edge_factor)
return array(c_p_lower_bound)
def edge_factor(self, wind_zone, panel_type):
# corner, north/south, east/west, middle
edge_matrix = {'A': [1.2, 1.2, 1.0, 1.0],
'B': [1.2, 1.2, 1.0, 1.0],
'C': [1.4, 1.4, 1.0, 1.0],
'D': [1.4, 1.4, 1.0, 1.0],
'E': [1.5, 1.0, 1.5, 1.0],
'F': [1.3, 1.3, 1.0, 1.0],
'G': [1.0, 1.0, 1.0, 1.0],
'H': [1.0, 1.0, 1.0, 1.0],
'I': [1.0, 1.0, 1.0, 1.0],
'J': [2.0, 2.0, 1.0, 1.0],
'K': [1.4, 1.4, 1.0, 1.0]}
return edge_matrix[wind_zone][panel_type.index()]
def racking_capacity(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.EastWest:
return minimum_racking_capacity
else:
return 308
def c_p_constants(self, A_n, wind_zone):
if wind_zone == 'A':
if A_n < 421.5:
return -0.2245, 1.717
elif A_n < 2294.7:
return -0.1298, 1.1446
else:
return -0.0308, 0.3787
elif wind_zone == 'B':
if A_n < 421.5:
return -0.1818, 1.3585
elif A_n < 2294.7:
return -0.0708, 0.688
else:
return -0.0308, 0.3787
elif wind_zone == 'C':
if A_n < 421.5:
return -0.0735, 0.6443
elif A_n < 2294.7:
return -0.059, 0.5566
else:
return -0.021, 0.2627
elif wind_zone == 'D':
if A_n < 187.3:
return -0.1007, 0.6868
elif A_n < 421.5:
return -0.049, 0.4181
else:
return -0.0183, 0.2304
elif wind_zone == 'E':
if A_n < 187.3:
return -0.058, 0.4236
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
elif wind_zone == 'F':
if A_n < 187.3:
return -0.0655, 0.4682
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
elif wind_zone == 'G':
if A_n < 187.3:
return -0.0341, 0.2786
elif A_n < 421.5:
return -0.0271, 0.242
else:
return -0.0125, 0.1533
elif wind_zone == 'H':
if A_n < 187.3:
return -0.1161, 0.7872
elif A_n < 421.5:
return -0.074, 0.5672
elif A_n < 1685.9:
return -0.0433, 0.3816
else:
return -0.0117, 0.1473
elif wind_zone == 'I':
if A_n < 187.3:
return -0.3856, 2.258
elif A_n < 421.5:
return -0.148, 1.0143
elif A_n < 1685.9:
return -0.0433, 0.3816
else:
return -0.0117, 0.1473
elif wind_zone == 'J':
if A_n < 187.3:
return -0.1024, 0.6557
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
else:
return 1, 1
def base_weight(self, panel_type, tray_count):
return [[54.50, 54.50, 56.80, 59.10],
[49.47, 49.47, 51.77, 54.07],
[53.42, 55.72, 58.02, 60.32],
[48.75, 51.05, 53.35, 55.65]][panel_type.index()][tray_count]
def link_tray_thresholds(self, panel_type):
return [[0, 12.0],
[0, 9.00],
[6, 13.5],
[5, 10.0]][panel_type.index()]
def cross_tray_thresholds(self, panel_type):
return [[12.0, 21.0, 30.0],
[9.00, 18.0, 27.0],
[13.5, 22.5, 31.5],
[10.0, 19.0, 28.0]][panel_type.index()]
def weighted_average_c_p(self, corner, north_south, east_west, middle):
""" Based on a 30 by 28 rectangular array """
return (middle * 182. + north_south * 14. + east_west * 13. + corner) / 210.
def minimum_a_n_coefficients(self, uplift_c_p, wind_zone):
if wind_zone == 'A':
if uplift_c_p > 0.360:
return 3.1178, 0.3769
elif uplift_c_p > 0.180:
return 2.4967, 0.274
else:
return 0.9691, 0.0685
elif wind_zone == 'B':
if uplift_c_p > 0.260:
return 2.3762, 0.2807
elif uplift_c_p > 0.160:
return 1.7699, 0.1803
else:
return 0.7209, 0.0392
elif wind_zone == 'C':
if uplift_c_p > 0.200:
return 1.7251, 0.215
elif uplift_c_p > 0.100:
return 1.2668, 0.1274
else:
return 0.4655, 0.0196
elif wind_zone == 'D':
if uplift_c_p > 0.120:
return 2.3762, 0.2807
elif uplift_c_p > 0.094:
return 1.7699, 0.1803
else:
return 0.7209, 0.0392
elif wind_zone == 'E':
if uplift_c_p > 0.078:
return 0.8096, 0.0976
elif uplift_c_p > 0.060:
return 0.438, 0.0361
else:
return 0.3227, 0.0206
elif wind_zone == 'F':
if uplift_c_p > 0.078:
return 0.6281, 0.0708
else:
return 0.328, 0.0212
elif wind_zone == 'G':
if uplift_c_p > 0.078:
return 0.6281, 0.0708
else:
return 0.328, 0.0212
elif wind_zone == 'H':
if uplift_c_p > 0.180:
return 1.8215, 0.2525
elif uplift_c_p > 0.120:
return 1.4679, 0.185
elif uplift_c_p > 0.060:
return 1.13479, 0.1298
else:
return 0.3227, 0.0206
elif wind_zone == 'I':
if uplift_c_p > 0.240:
return 4.3609, 0.6996
elif uplift_c_p > 0.120:
return 2.639, 0.3699
elif uplift_c_p > 0.060:
return 1.4027, 0.1659
else:
return 0.3277, 0.0206
elif wind_zone == 'J':
if uplift_c_p > 0.120:
return 0.7131, 0.0891
elif uplift_c_p > 0.078:
return 0.5503, 0.058
else:
return 0.328, 0.0212
elif wind_zone == 'K':
if uplift_c_p > 0.080:
return 0.3, 0.0455
else:
return 0.2465, 0.0212
else:
return 1, 1

View File

@@ -0,0 +1,235 @@
from numpy import array
from numpy.ma import log
from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
from helix.constants.system_type_constants.single_tilt_constants import SingleTiltConstants
class SingleTiltPSeriesConstants(object):
panel_spacing = (82.0, 61.8755) # inches
presenter_spacing = (1.5, 1)
panel_area = 22.22
surface_area = 22.22
tributary_area = array([1.87, 3.24, 3.24, 3.82])
ground_coverage_ratio = 0.67
friction_coefficient = 0.42
seismic_anchor_interval_constants = (8.9964, 2.09916)
max_psf = 32
system_constants = SingleTiltConstants()
def c_p_lower_bound(self):
c_p_lower_bound = []
for index, area in enumerate(self.tributary_area):
if area < 9:
c_p = -0.0273 * log(area) + 0.1401
else:
c_p = -0.0146 * log(area) + 0.112
edge_factor = self.edge_factor(self.system_constants.wind_zones[-1], PanelType.from_index(index))
c_p_lower_bound.append(c_p * edge_factor)
return array(c_p_lower_bound)
def edge_factor(self, wind_zone, panel_type):
# corner, north/south, east/west, middle
edge_matrix = {'A': [1.2, 1.2, 1.0, 1.0],
'B': [1.2, 1.2, 1.0, 1.0],
'C': [1.4, 1.4, 1.0, 1.0],
'D': [1.4, 1.4, 1.0, 1.0],
'E': [1.5, 1.0, 1.5, 1.0],
'F': [1.3, 1.3, 1.0, 1.0],
'G': [1.0, 1.0, 1.0, 1.0],
'H': [1.0, 1.0, 1.0, 1.0],
'I': [1.0, 1.0, 1.0, 1.0],
'J': [2.0, 2.0, 1.0, 1.0],
'K': [1.4, 1.4, 1.0, 1.0]}
return edge_matrix[wind_zone][panel_type.index()]
def racking_capacity(self, panel_type):
if panel_type == PanelType.Corner or panel_type == PanelType.EastWest:
return minimum_racking_capacity
else:
return 308
def c_p_constants(self, A_n, wind_zone):
if wind_zone == 'A':
if A_n < 421.5:
return -0.2245, 1.717
elif A_n < 2294.7:
return -0.1298, 1.1446
else:
return -0.0308, 0.3787
elif wind_zone == 'B':
if A_n < 421.5:
return -0.1818, 1.3585
elif A_n < 2294.7:
return -0.0708, 0.688
else:
return -0.0308, 0.3787
elif wind_zone == 'C':
if A_n < 421.5:
return -0.0735, 0.6443
elif A_n < 2294.7:
return -0.059, 0.5566
else:
return -0.021, 0.2627
elif wind_zone == 'D':
if A_n < 187.3:
return -0.1007, 0.6868
elif A_n < 421.5:
return -0.049, 0.4181
else:
return -0.0183, 0.2304
elif wind_zone == 'E':
if A_n < 187.3:
return -0.058, 0.4236
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
elif wind_zone == 'F':
if A_n < 187.3:
return -0.0655, 0.4682
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
elif wind_zone == 'G':
if A_n < 187.3:
return -0.0341, 0.2786
elif A_n < 421.5:
return -0.0271, 0.242
else:
return -0.0125, 0.1533
elif wind_zone == 'H':
if A_n < 187.3:
return -0.1161, 0.7872
elif A_n < 421.5:
return -0.074, 0.5672
elif A_n < 1685.9:
return -0.0433, 0.3816
else:
return -0.0117, 0.1473
elif wind_zone == 'I':
if A_n < 187.3:
return -0.3856, 2.258
elif A_n < 421.5:
return -0.148, 1.0143
elif A_n < 1685.9:
return -0.0433, 0.3816
else:
return -0.0117, 0.1473
elif wind_zone == 'J':
if A_n < 187.3:
return -0.1024, 0.6557
elif A_n < 421.5:
return -0.0518, 0.391
else:
return -0.0125, 0.1533
else:
return 1, 1
def base_weight(self, panel_type, tray_count):
if panel_type == PanelType.Corner:
return [66.91, 66.91, 70.09, 73.27][tray_count]
elif panel_type == PanelType.NorthSouth:
return [60.8, 60.8, 63.98, 67.16][tray_count]
elif panel_type == PanelType.EastWest:
return [64.75, 67.05, 70.23, 73.41][tray_count]
else:
return [60.08, 62.38, 65.56, 68.74][tray_count]
def link_tray_thresholds(self, panel_type):
return [[0, 13.0],
[0, 10.00],
[7, 14.5],
[6, 11.0]][panel_type.index()]
def cross_tray_thresholds(self, panel_type):
return [[13.0, 21.0, 29.0],
[10.00, 18.0, 26.0],
[14.5, 22.5, 30.5],
[11.0, 19.0, 27.0]][panel_type.index()]
def weighted_average_c_p(self, corner, north_south, east_west, middle):
""" Based on a 30 by 28 rectangular array """
return (middle * 182. + north_south * 14. + east_west * 13. + corner) / 210.
def minimum_a_n_coefficients(self, uplift_c_p, wind_zone):
if wind_zone == 'A':
if uplift_c_p > 0.360:
return 3.1178, 0.3769
elif uplift_c_p > 0.180:
return 2.4967, 0.274
else:
return 0.9691, 0.0685
elif wind_zone == 'B':
if uplift_c_p > 0.260:
return 2.3762, 0.2807
elif uplift_c_p > 0.160:
return 1.7699, 0.1803
else:
return 0.7209, 0.0392
elif wind_zone == 'C':
if uplift_c_p > 0.200:
return 1.7251, 0.215
elif uplift_c_p > 0.100:
return 1.2668, 0.1274
else:
return 0.4655, 0.0196
elif wind_zone == 'D':
if uplift_c_p > 0.120:
return 2.3762, 0.2807
elif uplift_c_p > 0.094:
return 1.7699, 0.1803
else:
return 0.7209, 0.0392
elif wind_zone == 'E':
if uplift_c_p > 0.078:
return 0.8096, 0.0976
elif uplift_c_p > 0.060:
return 0.438, 0.0361
else:
return 0.3227, 0.0206
elif wind_zone == 'F':
if uplift_c_p > 0.078:
return 0.6281, 0.0708
else:
return 0.328, 0.0212
elif wind_zone == 'G':
if uplift_c_p > 0.078:
return 0.6281, 0.0708
else:
return 0.328, 0.0212
elif wind_zone == 'H':
if uplift_c_p > 0.180:
return 1.8215, 0.2525
elif uplift_c_p > 0.120:
return 1.4679, 0.185
elif uplift_c_p > 0.060:
return 1.13479, 0.1298
else:
return 0.3227, 0.0206
elif wind_zone == 'I':
if uplift_c_p > 0.240:
return 4.3609, 0.6996
elif uplift_c_p > 0.120:
return 2.639, 0.3699
elif uplift_c_p > 0.060:
return 1.4027, 0.1659
else:
return 0.3277, 0.0206
elif wind_zone == 'J':
if uplift_c_p > 0.120:
return 0.7131, 0.0891
elif uplift_c_p > 0.078:
return 0.5503, 0.058
else:
return 0.328, 0.0212
elif wind_zone == 'K':
if uplift_c_p > 0.080:
return 0.3, 0.0455
else:
return 0.2465, 0.0212
else:
return 1, 1

View File

@@ -0,0 +1,46 @@
from enum import Enum
class PanelType(Enum):
Corner = 0
NorthSouth = 1
EastWest = 2
Middle = 3
@classmethod
def from_number(cls, number):
if number == 1:
return cls.Corner
elif number == 2:
return cls.NorthSouth
elif number == 3:
return cls.EastWest
elif number == 4:
return cls.Middle
return None
@classmethod
def from_index(cls, index):
return PanelType(index)
@classmethod
def all(cls):
return [cls.Corner, cls.NorthSouth, cls.EastWest, cls.Middle]
def number(self):
return self.value + 1
def index(self):
return self.value
def snake_case(self):
return {
PanelType.Corner: 'corner',
PanelType.NorthSouth: 'north_south',
PanelType.EastWest: 'east_west',
PanelType.Middle: 'middle',
}[self]
def __sub__(self, other): # Used for testing (assert_array_almost_equal requires subtraction)
return self.value - other.value

241
helix/constants/parts.py Normal file
View File

@@ -0,0 +1,241 @@
# Mechanical Parts
single_tilt_chassis = ('513831', 'CHASSIS, SINGLE TILT, HELIX ROOF')
dual_tilt_chassis = ('514056', 'BASE, CHASSIS, DUAL TILT, HELIX ROOF')
dual_tilt_platform = ('514057', 'PLATFORM, CHASSIS, DUAL TILT, HELIX ROOF')
platform_bolt = ('515063', 'SCREW, CAP, SH, M6 X 1 X 12, 18-8 SS (DIN 912)')
left_deflector = ('513841', 'DEFLECTOR, LH, HELIX ROOF')
right_deflector = ('513842', 'DEFLECTOR, RH, HELIX ROOF')
front_skirt = ('515928', 'FRONT SKIRT, HELIX ROOF')
rear_skirt = ('515929', 'REAR SKIRT, HELIX ROOF')
spoiler = ('513836', 'SPOILER, SINGLE TILT, HELIX ROOF')
rear_skirt_1_1 = ('520301', 'REAR SKIRT, HELIX ROOF V1.1')
spoiler_1_1 = ('520302', 'SPOILER, SINGLE TILT, HELIX ROOF V1.1')
front_skirt_1_1 = ('520303', 'FRONT SKIRT, HELIX ROOF V1.1')
cross_tray_1_1 = ('520306', 'TRAY, OPTIONAL BALLAST, HELIX ROOF V1.1')
left_deflector_1_1 = ('521794', 'DEFLECTOR, LH, HELIX ROOF V1.1')
right_deflector_1_1 = ('521795', 'DEFLECTOR, RH, HELIX ROOF V1.1')
leading_tray = ('517871', 'TRAY, LEADING, HELIX ROOF, RIVETED VERSION')
following_tray = ('513832', 'TRAY, FOLLOWING, HELIX ROOF')
link_tray = ('513833', 'TRAY, LINK, HELIX ROOF')
cross_tray = ('513844', 'TRAY, OPTIONAL BALLAST, HELIX ROOF')
anchor_plate = ('513843', 'PLATE, ANCHOR, HELIX ROOF')
anchor = ('TBD', 'Anchors')
anchor_washer = ('518477', 'WASHER, FLAT, 3/8, 1.00 OD, 18-8 SS')
module = ('TBD', 'Modules')
ballast = ('Contractor Supplied', 'Ballast Blocks')
rubber_foot = ('514265', 'FOOT, RECYCLED RUBBER, HELIX ROOF')
# Electrical Parts, per inverter
flat_washer = ('104813', 'WASHER, FLAT, 3/8, .812 OD, 18-8 SS (1500-731)')
channel_nut = ('106925', 'NUT, CHANNEL, 3/8-16, SS, UNISTRUT P3008 (1507-770)')
hex_nut_three_eighths_16 = ('107551', 'NUT, HEX, 3/8-16, 18-8 SS (5100-086)')
hex_bolt_3_4 = ('513007', 'BOLT, HH, 3/8-16 X 3/4, 316 SS')
hex_bolt_1_2 = ('514865', 'BOLT, HH, 3/8-16 X 1/2, 18-8 SS')
front_legs = ('512660', 'FRONT LEG, INVERTER RACK, HELIX ROOF')
back_legs = ('512661', 'BACK LEGS, INVERTER RACK, HELIX ROOF')
harness_2_string_fm = ('514437', 'HARNESS, DC COMBINATION, NO FUSE, 2 STRING, FEMALES TO MALE, HELIX')
harness_2_string_mf = ('514438', 'HARNESS, DC COMBINATION, NO FUSE, 2 STRING, MALES TO FEMALE, HELIX')
harness_3_string_fm = ('514435', 'HARNESS, DC COMBINATION, W/ FUSE, 3 STRING, FEMALES TO MALE, HELIX')
harness_3_string_mf = ('514436', 'HARNESS, DC COMBINATION, W/ FUSE, 3 STRING, MALES TO FEMALE, HELIX')
harness_4_string_fm = ('514439', 'HARNESS, DC COMBINATION, W/ FUSE, 4 STRING, FEMALES TO MALE, HELIX')
harness_4_string_mf = ('514440', 'HARNESS, DC COMBINATION, W/ FUSE, 4 STRING, MALES TO FEMALE, HELIX')
inverter_rail = ('512663', 'RAIL, INVERTER RACK, HELIX ROOF')
inverter_link = ('512662', 'LINK TO ARRAY, INVERTER RACK, HELIX ROOF')
inverter_link_short = ('521798', 'LINK TO ARRAY, LONG, INVERTER RACK, HELIX ROOF V1.1')
inverter_link_long = ('521797', 'LINK TO ARRAY, SHORT, INVERTER RACK, HELIX ROOF V1.1')
mounting_back_plate = ('518331', 'MOUNTING BACK PLATE, INVERTER/PANEL BOARD, HELIX ROOF/TRACKER')
sma_12kw_inverter = ('514686', 'INVERTER, SMA, STP, 12000TL-US-10 (SPR-12000m-3 XXX), AFCI, CONNECTORIZED')
sma_15kw_inverter = ('514687', 'INVERTER, SMA, STP, 15000TL-US-10 (SPR-15000m-3 XXX), AFCI, CONNECTORIZED')
sma_20kw_inverter = ('512676', 'INVERTER, SMA, STP, 20000TL-US-10 (SPR-20000m-3 XXX), AFCI, CONNECTORIZED')
sma_24kw_inverter = ('514685', 'INVERTER, SMA, STP, 24000TL-US-10 (SPR-24000m-3 XXX), AFCI, CONNECTORIZED')
delta_36kw_inverter = ('524952', 'INVERTER, DELTA, M36U_122(MC4), 10INPUT, 36KW, 3PH 480V AC,1000V DC')
delta_42kw_inverter = ('524969', 'INVERTER, DELTA, M42U_122(MC4), 12INPUT, 42KW, 3PH 480V AC,1000V DC')
delta_60kw_inverter = ('524954', 'INVERTER, DELTA, M60U_122 (MC4), 18INPUT, 60KW, 3PH 480V AC,1000V DC')
delta_80kw_inverter = ('524955', '-')
screw_12_24x1_25 = ('507985', 'SCREW, S-D, HWH, #12-24X 1-1/4", #3 PT, BI-METAL')
# Wire management
stump = ('512021', 'STUMP, WIRE MANAGEMENT, 50MM ID, HELIX ROOF')
cable_support = ('512511', 'CABLE SUPPORT, HELIX ROOF')
cable_support_lid = ('512510', 'LID, CABLE SUPPORT, HELIX ROOF')
wire_clip = ('512200', 'CLIP, WIRE FORMED, CABLE MANAGEMENT, INSIDE, 352MM ^ 2')
wire_clip_large = ('512199', 'CLIP, WIRE FORMED, CABLE MANAGEMENT, INSIDE, 1624MM ^ 2')
# Panel Boards
panel_board_4 = ('513299', 'COMBINER BOX, AC, 4 INPUT, NO AUX, W/ CONNECTOR')
panel_board_3 = ('513301', 'COMBINER BOX, AC, 3 INPUT, NO AUX, W/ CONNECTOR')
panel_board_2 = ('513303', 'COMBINER BOX, AC, 2 INPUT, NO AUX, W/ CONNECTOR')
harness_ac_inner = ('514477', 'HARNESS, AC, INNER, 72", HELIX ROOFTOP')
harness_ac_outer = ('514478', 'HARNESS, AC, OUTER, 108", HELIX ROOFTOP')
whip_tray = ('515059', 'ASSY, WHIP TRAY W/FUSE CLIPS, INVERTER, HELIX')
comm_cable = ('514697', 'COMM CABLE, INVERTER DAISY CHAIN, 118", HELIX ROOF')
ethernet_plug = ('518058', 'CONNECTOR, ETHERNET, PLUG, RJ-45, WEATHERPROOF, SHIELDED')
# Aux Plug
panel_board_4_aux = ('513300', 'COMBINER BOX, AC, 4 INPUT, W/ AUX, W/ CONNECTOR')
panel_board_3_aux = ('513302', 'COMBINER BOX, AC, 3 INPUT, W/ AUX, W/ CONNECTOR')
panel_board_2_aux = ('513304', 'COMBINER BOX, AC, 2 INPUT, W/ AUX, W/ CONNECTOR')
# Monitoring
monitor_power_plug = ('519008', 'HARNESS, MONITORING POWER CABLE, SINGLE CONNECTOR, HELIX ROOF')
monitor_controller_480_v = ('518059', 'CONTROLLER, MONITORING, COMMERCIAL, PVS5C BASED, 480VAC, US')
monitor_controller_240_v = ('517463', 'MONITORING SYSTEM, COMMERCIAL, <100KW, PVS5x BASED, 240VAC, US')
# DC Switch related
dc_switch_bracket = ('512575', 'BRACKET, DC SWITCH BOX, HELIX')
dc_switch_box = ('514698', 'DC SWITCH BOX, HELIX')
hex_bolt_quarter_20 = ('114961', 'BOLT, HH, 1/4-20 X 3/4", 18-8SS')
hex_nut_quarter_20 = ('107549', 'NUT, HEX, 1/4-20, 18-8 SS (5100-084)')
flat_washer_quarter_inch = ('107586', 'WASHER, FLAT, 1/4, 0.5 OD, 18-8 SS (5100-144)')
# Standalone Inverter
star_washer = ('105317', 'WASHER, STAR, #6, SS (1501-606)')
flat_washer_6 = ('111147', 'WASHER, FLAT, #6, 18-8 SS (1509-097)')
phillips_screw = ('107538', 'SCREW, PH, 6-32 X 1/2, SS (5100-073)')
ac_splice_box = ('516045', 'AC SPLICE BOX, CONNECTORIZED, HELIX ROOF')
ac_switch = ('516043', 'AC SWITCH, CONNECTORIZED, HELIX ROOF')
ac_inverter_bracket = ('513586', 'BRACKET, INVERTER AC SWITCH, HELIX')
delta_kit_inverter_mount = ('524781', 'KIT, INVERTER MOUNT, DELTA, HELIX ROOF')
delta_kit_inverter_mount_dt = ('525772', 'KIT, INVERTER MOUNT, DELTA, DT, HELIX ROOF')
delta_splice_box = ('525651', 'KIT, AC SPLICE, DELTA, HELIX ROOF')
delta_inverter_leg = ('524783', 'INVERTER LEG, DELTA, HELIX ROOF')
delta_branch_connector = ('TBD', 'Branch connector')
# Other Ebom
sunshade = ('512910', 'SUN SHADE, INVERTER, HELIX')
sunshade_bolt = ('805615', 'SCREW, HEXAGONAL HEAD, M10X20, SS A2')
sunshade_washer = ('521031', 'WASHER, FLAT, M10 X 20MM OD, SS')
fuseshade = ('521363', 'FUSE SHADE, HELIX ROOF')
fuseshade_brace = ('522020', 'BRACE, FUSE SHADE, HELIX ROOF')
# Package Sizes
package_sizes = {
flat_washer: 50,
flat_washer_quarter_inch: 100,
channel_nut: 50,
hex_nut_three_eighths_16: 50,
hex_nut_quarter_20: 100,
hex_bolt_3_4: 50,
hex_bolt_quarter_20: 50,
hex_bolt_1_2: 50,
wire_clip_large: 10,
wire_clip: 30,
platform_bolt: 50,
star_washer: 100,
anchor_washer: 25
}
all_parts = [
single_tilt_chassis,
dual_tilt_chassis,
dual_tilt_platform,
platform_bolt,
left_deflector,
right_deflector,
front_skirt,
rear_skirt,
spoiler,
rear_skirt_1_1,
spoiler_1_1,
front_skirt_1_1,
cross_tray_1_1,
leading_tray,
following_tray,
link_tray,
cross_tray,
anchor_plate,
anchor,
anchor_washer,
module,
ballast,
rubber_foot,
flat_washer,
channel_nut,
hex_nut_three_eighths_16,
hex_bolt_3_4,
hex_bolt_1_2,
front_legs,
back_legs,
harness_2_string_fm,
harness_2_string_mf,
harness_3_string_fm,
harness_3_string_mf,
harness_4_string_fm,
harness_4_string_mf,
inverter_rail,
inverter_link,
mounting_back_plate,
sma_12kw_inverter,
sma_15kw_inverter,
sma_20kw_inverter,
sma_24kw_inverter,
stump,
cable_support,
cable_support_lid,
wire_clip,
wire_clip_large,
panel_board_4,
panel_board_3,
panel_board_2,
harness_ac_inner,
harness_ac_outer,
whip_tray,
comm_cable,
ethernet_plug,
panel_board_4_aux,
panel_board_3_aux,
panel_board_2_aux,
monitor_power_plug,
monitor_controller_480_v,
dc_switch_bracket,
dc_switch_box,
hex_bolt_quarter_20,
hex_nut_quarter_20,
flat_washer_quarter_inch,
star_washer,
flat_washer_6,
phillips_screw,
ac_splice_box,
ac_switch,
ac_inverter_bracket,
sunshade,
sunshade_bolt,
sunshade_washer,
screw_12_24x1_25,
fuseshade_brace,
left_deflector_1_1,
right_deflector_1_1,
inverter_link_short,
inverter_link_long,
fuseshade,
monitor_controller_240_v
]

View File

@@ -0,0 +1,7 @@
import os
from helix.db.redis_manager import RedisManager
vcap_url = os.getenv('VCAP_SERVICES') # pws is best ws.
heroku_url = os.getenv('REDIS_URL')
redis_store = RedisManager.get_redis_connection(vcap_url, heroku_url)

View File

@@ -0,0 +1,5 @@
from enum import Enum
class SeismicAnchorValidationError(Enum):
TooFewAnchors = 'There are too few anchors in one or more subarrays'

View File

@@ -0,0 +1,94 @@
from helix.constants.module_type import ModuleType
from helix.constants.parts import *
class SingleTiltParts(object):
center_panel_parts = {
module: 1,
single_tilt_chassis: 1
}
sub_array_parts = {
leading_tray: 1
}
north_south_panel_parts = {
module: 1,
single_tilt_chassis: 0.5,
}
def __init__(self, module_type):
if module_type == ModuleType.PSeries:
self.corner_panel_parts = {
module: 1,
single_tilt_chassis: 1,
left_deflector_1_1: 0.5,
right_deflector_1_1: 0.5,
}
self.east_west_panel_parts = {
module: 1,
single_tilt_chassis: 1.5,
left_deflector_1_1: 0.5,
right_deflector_1_1: 0.5
}
else:
self.corner_panel_parts = {
module: 1,
single_tilt_chassis: 1,
left_deflector: 0.5,
right_deflector: 0.5
}
self.east_west_panel_parts = {
module: 1,
single_tilt_chassis: 1.5,
left_deflector: 0.5,
right_deflector: 0.5
}
def row_parts(self, _):
return {}
def column_parts(self, module_type):
if module_type == ModuleType.Cell96:
front_skirt_parts = front_skirt
else:
front_skirt_parts = front_skirt_1_1
return {
front_skirt_parts: 1,
leading_tray: 1
}
def parts_per_panel_type(self):
return [
self.corner_panel_parts,
self.north_south_panel_parts,
self.east_west_panel_parts,
self.center_panel_parts
]
def module(self, module_type):
if module_type == ModuleType.Cell96:
rear_skirt_parts = rear_skirt
spoiler_parts = spoiler
else:
rear_skirt_parts = rear_skirt_1_1
spoiler_parts = spoiler_1_1
return {
spoiler_parts: 1,
rear_skirt_parts: 1,
wire_clip: 2,
rubber_foot: 0.1
}
def dependent_parts(self, module_type):
return {
module: self.module(module_type),
leading_tray: {
following_tray: 1
}
}
def fudge_factors(self, _):
return {
single_tilt_chassis: 1.0525
}

View File

@@ -0,0 +1,6 @@
import os
from helix.db.sql_manager import SQLManager
def sql_session_maker():
return SQLManager.get_sql_session_maker(os.getenv('DATABASE_URL'))

View File

@@ -0,0 +1,6 @@
'''
Created on May 22, 2017
@author: jvazquez
'''
SUBARRAY_SIZE_BIG = "Array size is too big. Max is 150' by 150'."

View File

@@ -0,0 +1,60 @@
from enum import Enum
from helix.constants.module_type import ModuleType
from helix.constants.module_type_constants.dual_tilt_128_cell_constants import DualTilt128CellConstants
from helix.constants.module_type_constants.dual_tilt_96_cell_constants import DualTilt96CellConstants
from helix.constants.dual_tilt_parts import DualTiltParts
from helix.constants.module_type_constants.dual_tilt_pseries_constants import DualTiltPSeriesConstants
from helix.constants.module_type_constants.single_tilt_128_cell_constants import SingleTilt128CellConstants
from helix.constants.module_type_constants.single_tilt_96_cell_constants import SingleTilt96CellConstants
from helix.constants.module_type_constants.single_tilt_pseries_constants import SingleTiltPSeriesConstants
from helix.constants.single_tilt_parts import SingleTiltParts
from helix.constants.system_type_constants.dual_tilt_constants import DualTiltConstants
from helix.constants.system_type_constants.single_tilt_constants import SingleTiltConstants
class SystemType(Enum):
singleTilt = '0'
dualTilt = '1'
def system_constants(self):
return {
SystemType.singleTilt: SingleTiltConstants(),
SystemType.dualTilt: DualTiltConstants()
}[self]
def module_constants(self, module_type):
return {
SystemType.singleTilt: self.single_tilt_constants(module_type),
SystemType.dualTilt: self.dual_tilt_constants(module_type)
}[self]
def parts(self, module_type):
return {
SystemType.singleTilt: SingleTiltParts(module_type),
SystemType.dualTilt: DualTiltParts(module_type)
}[self]
def display_name(self):
return {
SystemType.singleTilt: 'Single-Tilt',
SystemType.dualTilt: 'Dual-Tilt'
}[self]
@classmethod
def default_value(cls):
return cls.dualTilt.value
def single_tilt_constants(self, module_type):
return {
ModuleType.Cell96: SingleTilt96CellConstants(),
ModuleType.Cell128: SingleTilt128CellConstants(),
ModuleType.PSeries: SingleTiltPSeriesConstants()
}[module_type]
def dual_tilt_constants(self, module_type):
return {
ModuleType.Cell96: DualTilt96CellConstants(),
ModuleType.Cell128: DualTilt128CellConstants(),
ModuleType.PSeries: DualTiltPSeriesConstants()
}[module_type]

View File

@@ -0,0 +1,4 @@
class DualTiltConstants(object):
wind_zones = ['A', 'B', 'C', 'D', 'E']
module_count = 2
minimum_corner_module_count = 2

View File

@@ -0,0 +1,4 @@
class SingleTiltConstants(object):
wind_zones = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']
module_count = 1
minimum_corner_module_count = 4

View File

@@ -0,0 +1,7 @@
import os
def version():
if os.getenv('VERSION'):
return os.getenv('VERSION')
return 'release-9-72-g3e1c132'

107
helix/csv_builder.py Normal file
View File

@@ -0,0 +1,107 @@
import csv
from enum import Enum
from io import StringIO
class PanelDataColumn(Enum):
Handle = 'HANDLE'
Blockname = 'BLOCKNAME'
Subarray = 'SUBARRAY'
PanelType = 'POS'
WindZone = 'WIND'
Ballast = 'BAL'
LinkTray = 'LT_CALCULATED'
CrossTray = 'XTRAY'
WindAnchor = 'ANC'
SeismicAnchor = 'SEISMIC'
Coordinate = 'COORDINATE'
Pressure = 'PSF'
Id = 'ID'
PresentedLinkTray = 'LTRAY'
Xcoord = 'XCOORD'
Ycoord = 'YCOORD'
Rotation = 'ANGLE'
FuzzyWindZone = 'FUZZYWINDZONE'
class CSVDataColumn(Enum):
PresentedAnchors = "ANC"
class CsvBuilder(object):
def build_cad_output(self, panels):
panels.sort(key=lambda x: x.id)
output_columns = [
PanelDataColumn.Handle, PanelDataColumn.Blockname, PanelDataColumn.WindZone, PanelDataColumn.PanelType,
PanelDataColumn.Subarray, PanelDataColumn.Pressure, PanelDataColumn.Ballast,
PanelDataColumn.PresentedLinkTray, PanelDataColumn.CrossTray, CSVDataColumn.PresentedAnchors,
PanelDataColumn.Id, PanelDataColumn.Xcoord, PanelDataColumn.Ycoord, PanelDataColumn.Rotation
]
use_fuzzy_wind_zone = False
for panel in panels:
if panel.fuzzy_wind_zone:
output_columns.append(PanelDataColumn.FuzzyWindZone)
use_fuzzy_wind_zone = True
break
header_row = [col.value for col in output_columns]
matrix = [self.format_panel_for_csv(panel, include_fuzzy_wind_zone=use_fuzzy_wind_zone) for panel in panels]
return self.output_csv(header_row, matrix)
def build_bom_output(self, rows):
headers = ['Part #', 'Description', 'Total']
return self.output_csv(headers, rows)
def output_csv(self, headers, rows):
output = StringIO()
writer = csv.writer(output, dialect='excel-tab', quoting=csv.QUOTE_NONE, quotechar='')
writer.writerow(headers)
writer.writerows(rows)
return output.getvalue()
def format_panel_for_csv(self, panel, include_fuzzy_wind_zone=False):
row = [
panel.handle or "",
panel.blockname or "",
self.int_to_wind_zone(panel.wind_zone),
panel.panel_type.number() if panel.panel_type else "",
panel.subarray,
round(panel.pressure, 2) if panel.pressure else "",
self.present_value(panel.ballast),
self.present_value(panel.presented_link_tray) or '-',
self.present_value(panel.cross_tray) or '-',
self.present_anchors(panel.wind_anchors, panel.seismic_anchors),
panel.id,
round(panel.original_coordinate.x, 14),
round(panel.original_coordinate.y, 14),
round(panel.original_coordinate.rotation, 14)
]
if include_fuzzy_wind_zone:
row.append(int(panel.fuzzy_wind_zone))
return row
def round_two_digits(self, value):
return round(value, 2)
def zero_to_dash(self, value):
return int(value) or '-'
def int_to_wind_zone(self, value):
if value is None:
return ""
return ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"][value]
def present_value(self, value, default=''):
if value is None:
return default
return int(value)
def present_anchors(self, wind, seismic):
if not wind and not seismic:
return '-'
wind_string = str(int(wind)) if wind != 0 else ''
return wind_string + 'S' * int(seismic)

0
helix/db/__init__.py Normal file
View File

17
helix/db/redis_manager.py Normal file
View File

@@ -0,0 +1,17 @@
import json
import redis
class RedisManager(object):
@staticmethod
def get_redis_connection(vcap_env, heroku_redis_url):
if vcap_env:
redis_env = json.loads(vcap_env)['rediscloud'][0]['credentials']
return redis.Redis(host=redis_env['hostname'],
port=int(redis_env['port']),
password=redis_env['password'])
elif heroku_redis_url:
return redis.Redis.from_url(heroku_redis_url)
else:
return redis.Redis(host='localhost', port=6379, db=0)

26
helix/db/sql_manager.py Normal file
View File

@@ -0,0 +1,26 @@
import sqlalchemy
from sqlalchemy.orm import sessionmaker
class SQLManager(object):
# Cache the database connection per application process.
# More properly, this should be kept in thread-local state (threading.local()), but should suffice.
# Each passed connection url will have its own pool.
engines = {}
@classmethod
def get_sql_session_maker(cls, heroku_postgres_url, should_echo=False, cache=True):
if not cache:
return sessionmaker(bind=cls.connect(heroku_postgres_url, should_echo=should_echo))()
else:
engine = cls.engines.setdefault(heroku_postgres_url,
cls.connect(heroku_postgres_url, should_echo=should_echo))
return sessionmaker(bind=engine)()
@staticmethod
def connect(heroku_postgres_url, should_echo=False):
if heroku_postgres_url:
return sqlalchemy.create_engine(heroku_postgres_url, echo=should_echo)
else:
return sqlalchemy.create_engine('postgres://pivotal:@localhost/pivotal', echo=True)

View File

@@ -0,0 +1,166 @@
from base64 import b64encode
import os
from helix.constants.system_type import SystemType
from helix.constants.version import version
class DocGenParamsBuilder(object):
def __init__(self, user_values, system_type, calculator, image_presenter):
self.user_values = user_values
self.system_type = system_type
self.calculator = calculator
self.image_presenter = image_presenter
def build(self):
if self.user_values.system_type() == SystemType.singleTilt:
template_name = 'Helix_Single_Tilt_Template'
else:
template_name = 'Helix_Dual_Tilt_Template'
body = {
**self.site_characterization(),
**self.calculator.documentation_summary_values(),
**self.panel_attributes(),
**self.bom(),
**self.power_stations(),
**self.standalone_inverters(),
**self.subarray_summary(),
}
image_dicts = self.array_image()
list_params = self.convert_dict_params_to_list(body)
images = self.convert_dict_params_to_list(image_dicts, key_name='imageKey', value_name='base64encodedImage')
api_key = os.getenv('SP_DOCGEN_API_KEY', '')
params = {
"apiKey": api_key,
"templateName": template_name,
"nameValuePairs": list_params,
"dynamicImages": images,
}
return params
def site_characterization(self):
panels = self.calculator.get_computed_csv_columns()
return {
'project_name': self.user_values.project_name(),
'building_height': self.user_values.building_height(),
'building_width': self.user_values.building_width(),
'building_length': self.user_values.building_length(),
'parapet_height': self.user_values.building_parapet_height(),
'ballast_block_weight': self.user_values.ballast_block_weight(),
'max_allowable_system_pressure': self.user_values.max_system_pressure(),
'anchor_type': self.user_values.anchor_type().value,
'exposure_category': self.user_values.exposure_category().value,
'exposure_category_transition_distance': self.user_values.exposure_category_transition_distance(),
'wind_speed': self.user_values.wind_speed(),
'spectral_response': self.user_values.spectral_response(),
'seismic_importance_factor': self.user_values.importance_factor(),
'module_type': self.user_values.module_type().value,
'system_type': self.user_values.system_type().display_name(),
'total_modules': len(panels),
'version': version(),
'lb': round(self.calculator.L_B(), 2),
'kz': round(self.calculator.k_z(), 2),
'qz': round(self.calculator.q_z(), 2),
}
def panel_attributes(self):
summary_table = self.calculator.summary_table()
minimum_array_sizes = self.calculator.minimum_array_sizes()
result = {}
for panel_type, values in summary_table.items():
ballast_blocks = values['ballast blocks']
pressure = values['pressure']
anchors = values['anchors']
for wind_zone_index in range(len(ballast_blocks)):
wind_zone = self.system_type.system_constants().wind_zones[wind_zone_index].lower()
prefix = '%d_%s_' % (panel_type.number(), wind_zone)
result[prefix + 'bb'] = ballast_blocks[wind_zone_index]
result[prefix + 'psf'] = pressure[wind_zone_index]
result[prefix + 'anc'] = anchors[wind_zone_index]
for idx, array_size in enumerate(minimum_array_sizes):
wind_zone = self.system_type.system_constants().wind_zones[idx].lower()
result['min_' + wind_zone] = array_size
return result
def bom(self):
bom_values = self.calculator.documentation_bom()
result = {}
for row in bom_values:
result['total_' + row[0]] = row[1]
return result
def power_stations(self):
power_station_string = ""
power_stations = self.user_values.power_stations()
if len(power_stations) > 0:
power_station_string = "\n"
for power_station in self.user_values.power_stations():
power_station_string += "Description: %s\n" % power_station['power_station_description']
power_station_string += "\tQuantity: %s\n" % power_station['power_station_quantity']
power_station_string += "\tAC Run Length: %s\n" % power_station['ac_run_length']
power_station_string += "\tInverters:\n"
for inverter in power_station['inverters']:
power_station_string += "\t\tModel: %s\n" % inverter['model'].label
power_station_string += "\t\tStrings per inverter: %s\n" % inverter['strings_per_inverter']
if inverter.get('sunshade'):
power_station_string += "\t\tSunShade: Yes\n"
if inverter.get('dc_switch'):
power_station_string += "\t\tDC Switch: Yes\n"
power_station_string += "\n"
return {'power_station_string': power_station_string}
def standalone_inverters(self):
inverters_string = ""
for inverter in self.user_values.standalone_inverters():
inverters_string += "\n"
inverters_string += "\tAC Run Length: %s\n" % inverter['ac_run_length']
inverters_string += "\tAttachment Point: %s\n" % inverter['attachment_point'][0]
inverters_string += "\tModel: %s\n" % inverter['model'].label
inverters_string += "\tStrings per inverter: %s\n" % inverter['strings_per_inverter']
if inverter.get('sunshade'):
inverters_string += "\tSunShade: Yes\n"
if inverter.get('dc_switch'):
inverters_string += "\tDC Switch: Yes\n"
return {'standalone_inverter_string': inverters_string}
def subarray_summary(self):
subarray_string = ""
for subarray in self.calculator.subarray_summary():
subarray_string += "\n"
subarray_string += "\tSubarray: %d\n" % subarray.subarray_number
subarray_string += "\tSeismic Anchors: %d\n" % subarray.required_seismic_anchors
subarray_string += "\tWeight: %s lbs\n" % "{:,}".format(round(subarray.weight))
return {'subarrays_string': subarray_string}
def array_image(self):
png_data = self.image_presenter.generate_image(self.calculator.get_computed_csv_columns(),
self.calculator.subarrays)
return {'array_image': b64encode(png_data).decode('utf-8')}
@staticmethod
def convert_list_params_to_dict(list_params):
dict_params = {}
for datum in list_params:
dict_params[datum['name']] = datum['value']
return dict_params
@staticmethod
def convert_dict_params_to_list(dict_params, key_name='name', value_name='value'):
list_params = []
for k, v in dict_params.items():
list_params.append({
key_name: k,
value_name: v
})
return sorted(list_params, key=lambda k: k[key_name])

0
helix/forms/__init__.py Normal file
View File

View File

@@ -0,0 +1,17 @@
from wtforms.validators import Optional
class ConditionalValidator(object):
def __init__(self, other_field_name, other_data_contents, dependent_validator):
self.other_field_name = other_field_name
self.other_data_contents = other_data_contents
self.dependent_validator = dependent_validator
def __call__(self, form, field):
other_field = form._fields.get(self.other_field_name)
if other_field is None:
raise Exception('no field named "%s" in form' % self.other_field_name)
if other_field.data in self.other_data_contents:
self.dependent_validator.__call__(form, field)
else:
Optional().__call__(form, field)

183
helix/forms/ebom_form.py Normal file
View File

@@ -0,0 +1,183 @@
from wtforms import StringField, SelectField, FormField, BooleanField
from wtforms.fields.html5 import IntegerField
from wtforms.validators import NumberRange, DataRequired
from helix.constants.inverter_brand import InverterBrand
from helix.constants.inverter_type import InverterType
from helix.constants.system_type import SystemType
from helix.forms.grouped_form import GroupedForm
def generate_string_choices(from_i, to_i, only_even=False):
step = 2 if only_even else 1
return list(
map(lambda x: (x, "%s" % x), range(from_i, to_i + 1, step))
)
class InverterBrandForm(GroupedForm):
form_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden', 'value': "inverter_brand_form"})
inverter_brand_id = SelectField('',
choices=[(InverterBrand.SMA.value, InverterBrand.SMA.label),
(InverterBrand.DELTA.value, InverterBrand.DELTA.label)],
coerce=int,
default=InverterBrand.default_value(),
render_kw={'group': 'inverter_brands'},
)
def populate_choices(self, inverter_brands):
if len(inverter_brands) > 0:
self.inverter_brand_id.default = next(
map(lambda x: x['inverter_brand_id'], inverter_brands)
, InverterBrand.default_value())
self.process()
def is_delta(self):
return self.inverter_brand_id.data == InverterBrand.DELTA.value
class InverterFormSMA(GroupedForm):
quantity = IntegerField('Quantity',
default=1,
render_kw={'group': 'quantity', 'row_class': 'quantity'},
validators=[NumberRange(0, None)])
model = SelectField('Model',
choices=[(InverterType.SMA.MODEL_12KW.value, InverterType.SMA.MODEL_12KW.label),
(InverterType.SMA.MODEL_15KW.value, InverterType.SMA.MODEL_15KW.label),
(InverterType.SMA.MODEL_20KW.value, InverterType.SMA.MODEL_20KW.label),
(InverterType.SMA.MODEL_24KW.value, InverterType.SMA.MODEL_24KW.label)],
coerce=int,
default=InverterType.SMA.default_value(),
render_kw={'group': 'non-optional', 'row_class': 'inverter_model'},
)
strings_per_inverter = SelectField('# Strings/Inverter',
coerce=int,
choices=generate_string_choices(2, 8),
default=8,
render_kw={'group': 'non-optional', 'row_class': 'inverter_strings'})
sunshade = BooleanField('Sun Shade', render_kw={'group': 'optional'})
dc_switch = BooleanField('DC Switch', render_kw={'group': 'optional'})
def update_strings(self, system_type):
self.strings_per_inverter.choices = generate_string_choices(
2,
8,
system_type != SystemType.singleTilt
)
class InverterFormDelta(GroupedForm):
quantity = IntegerField('Quantity',
default=1,
render_kw={'group': 'quantity', 'row_class': 'quantity'},
validators=[NumberRange(0, None)])
model = SelectField('Model',
choices=[(InverterType.DELTA.MODEL_36KW.value, InverterType.DELTA.MODEL_36KW.label),
(InverterType.DELTA.MODEL_42KW.value, InverterType.DELTA.MODEL_42KW.label),
(InverterType.DELTA.MODEL_60KW.value, InverterType.DELTA.MODEL_60KW.label),
# (InverterType.DELTA.MODEL_80KW.value, InverterType.DELTA.MODEL_80KW.label),
],
coerce=int,
default=InverterType.DELTA.default_value(),
render_kw={'group': 'non-optional', 'row_class': 'inverter_model'},
)
strings_per_inverter = SelectField('# Strings/Inverter',
coerce=int,
choices=generate_string_choices(0, 24),
default=8,
render_kw={'group': 'non-optional', 'row_class': 'inverter_strings'})
splice_box = BooleanField('Splice Box', default=True, render_kw={'group': 'optional'})
class EbomForm(GroupedForm):
power_station_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden'})
form_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden', 'value': 'power_station_form'})
power_station_description = StringField('Power Station Description', render_kw={'group': 'header'},
validators=[DataRequired(message='Power Station Description is required.')],
default='Power Station 1')
power_station_quantity = IntegerField('Power Station Quantity',
default=1,
validators=[NumberRange(0, None)],
render_kw={'group': 'header'})
ac_run_length = IntegerField('Total AC Run Length for Power Station(s) (ft)',
render_kw={'group': 'header'}, default=0, validators=[NumberRange(0, None)])
monitor_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden'})
inverter_quantity = SelectField('Inverters', choices=[(1, "1"), (2, "2"), (3, "3"), (4, "4")],
coerce=int,
default=4,
render_kw={'group': 'inverter_quantity'})
inverter_1 = FormField(InverterFormSMA, 'Inverter 1', render_kw={'group': 'inverters'})
inverter_2 = FormField(InverterFormSMA, 'Inverter 2', render_kw={'group': 'inverters'})
inverter_3 = FormField(InverterFormSMA, 'Inverter 3', render_kw={'group': 'inverters'})
inverter_4 = FormField(InverterFormSMA, 'Inverter 4', render_kw={'group': 'inverters'})
def update_inverter_strings_choices(self, system_type):
self.inverter_1.update_strings(system_type)
self.inverter_2.update_strings(system_type)
self.inverter_3.update_strings(system_type)
self.inverter_4.update_strings(system_type)
class StandAloneInverterForm(GroupedForm):
standalone_inverter_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden'})
form_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden', 'value': "standalone_inverter_form"})
standalone_ac_run_length = IntegerField('AC Run Length for Inverter (ft)',
render_kw={'group': 'power_station'},
default=0, validators=[NumberRange(0, None)])
def update_inverter_strings_choices(self, system_type):
self.inverter.update_strings(system_type)
def populate_choices(self):
pass
class StandAloneInverterFormSMA(StandAloneInverterForm):
inverter = FormField(InverterFormSMA, 'Inverter', render_kw={'group': 'inverters'})
attachment_point = SelectField('Attachment Point',
choices=[],
default='switch_gear',
render_kw={'group': 'power_station'})
def populate_choices(self, power_stations, standalone_inverters):
standalone_inverter_count_per_power_station = {}
for inverter in standalone_inverters:
key = inverter['attachment_point'][1]
standalone_count = standalone_inverter_count_per_power_station.get(key) or 0
standalone_inverter_count_per_power_station[key] = standalone_count + 1
power_stations_with_free_slots = []
for power_station in power_stations:
standalone_count = standalone_inverter_count_per_power_station.get(power_station['power_station_id']) or 0
inverter_count = power_station['inverter_quantity'] + standalone_count
if inverter_count < 4 and power_station['power_station_quantity'] == 1:
power_stations_with_free_slots.append(power_station)
choices = map(lambda x: (str(x['power_station_id']), x['power_station_description']), power_stations_with_free_slots)
self.attachment_point.choices = [('switch_gear', 'Switch Gear')] + list(choices)
class StandAloneInverterFormDelta(StandAloneInverterForm):
inverter = FormField(InverterFormDelta, 'Inverter', render_kw={'group': 'inverters'})
class SupervisorForm(GroupedForm):
monitor_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden'})
form_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden', 'value': "supervisor_form"})
power_source = SelectField('Power Source',
render_kw={'group': 'power_source'},
choices=[('switch_gear', 'Switch Gear/External')])
class SupervisorFormSMA(SupervisorForm):
def populate_choices(self, power_stations, supervisors):
supervisor_power_sources = list(map(lambda x: x['power_source'][1], supervisors))
power_stations_without_supervisors = []
for power_station in power_stations:
if power_station['power_station_id'] not in supervisor_power_sources:
power_stations_without_supervisors.append(power_station)
choices = map(lambda x: (str(x['power_station_id']), x['power_station_description']), power_stations_without_supervisors)
self.power_source.choices = self.power_source.choices[:1] + list(choices)

View File

@@ -0,0 +1,6 @@
from flask.ext.wtf import Form
class GroupedForm(Form):
def group(self, label):
return [field for field in self if field.render_kw and field.render_kw['group'] == label]

95
helix/forms/input_form.py Normal file
View File

@@ -0,0 +1,95 @@
from flask.ext.wtf.file import FileField
from helix.constants.anchor_type import AnchorType
from helix.constants.exposure_category import ExposureCategory
from helix.constants.module_type import ModuleType
from helix.constants.system_type import SystemType
from wtforms import SelectField, StringField, BooleanField
from wtforms.fields.html5 import DecimalField, IntegerField
from wtforms.validators import NumberRange, DataRequired
from helix.forms.conditional_validator import ConditionalValidator
from helix.forms.grouped_form import GroupedForm
class InputForm(GroupedForm):
project_name = StringField('Project Name', validators=[DataRequired(message='Project Name is required.')],
render_kw={'group': 'project_info'})
building_height = DecimalField('Building Height (ft)', places=1, validators=[NumberRange(0, None)],
render_kw={'group': 'site_info'})
building_width = DecimalField('Building Width (ft)', places=1, validators=[NumberRange(0, None)],
render_kw={'group': 'site_info'})
building_length = DecimalField('Building Length (ft)', places=1, validators=[NumberRange(0, None)],
render_kw={'group': 'site_info'})
building_parapet_height = DecimalField('Parapet Height (ft)', places=1, validators=[NumberRange(0, None)],
render_kw={'group': 'site_info'})
wind_speed = IntegerField('Wind Speed (ASCE 7-10) (mph)',
validators=[NumberRange(100, 200)],
render_kw={'group': 'site_info',
'link': {'text': 'Look up',
'href': 'http://windspeed.atcouncil.org/'}})
exposure_category = SelectField('Exposure Category',
choices=[(ExposureCategory.B.value, ExposureCategory.B.value),
(ExposureCategory.B_C.value, "B to C"),
(ExposureCategory.C_B.value, "C to B"),
(ExposureCategory.C.value, ExposureCategory.C.value),
(ExposureCategory.D.value, ExposureCategory.D.value)],
default=ExposureCategory.default_value(),
render_kw={'group': 'site_info',
'link': {'text': 'More info',
'href': '/exposure_categories'}
})
exposure_category_transition_distance = IntegerField('Exposure Transition Distance (ft)',
default=0,
validators=[ConditionalValidator('exposure_category',
['B to C', 'C to B'],
NumberRange(1, None))],
render_kw={'group': 'site_info'})
ballast_block_weight = DecimalField('Ballast Block Weight (lbs)',
validators=[NumberRange(12, 20)],
default=14,
places=1,
render_kw={'group': 'site_info'})
max_system_pressure = DecimalField('Max Allowable System Pressure (psf)',
places=1,
validators=[NumberRange(0, None)],
default=12,
render_kw={'group': 'site_info'})
system_type = SelectField('System Type',
choices=[(SystemType.singleTilt.value, SystemType.singleTilt.display_name()),
(SystemType.dualTilt.value, SystemType.dualTilt.display_name())],
default=SystemType.default_value(),
render_kw={'group': 'project_info'})
module_type = SelectField('Module Type',
choices=[(ModuleType.Cell128.value, ModuleType.Cell128.value),
(ModuleType.PSeries.value, ModuleType.PSeries.value),
(ModuleType.Cell96.value, ModuleType.Cell96.value)],
default=ModuleType.default_value(),
render_kw={'group': 'project_info'})
anchor_type = SelectField('Anchor Type',
choices=[(AnchorType.OMG_PowerGrip.value, AnchorType.OMG_PowerGrip.value),
(AnchorType.OMG_PowerGrip_Plus.value, AnchorType.OMG_PowerGrip_Plus.value),
(AnchorType.EcoFasten.value, AnchorType.EcoFasten.value)],
default=AnchorType.default_value(),
render_kw={'group': 'site_info', 'tooltip': 'OMG anchors are compatible with TPO and PVC roof membranes.<br>EcoFasten anchors are compatible with Built Up Roofing (BUR), Hot Tar, Sips Panels and membrane type roofs.'})
design_spectral_response = DecimalField('Design Spectral Response Acceleration (S<sub>DS</sub>) (g)',
places=1, validators=[NumberRange(0, 5)],
render_kw={'group': 'site_info',
'link': {'text': 'Look up',
'href': 'http://earthquake.usgs.gov/designmaps/us/application.php'
}
})
importance_factor = SelectField('Seismic Importance Factor (I<sub>p</sub>)',
choices=[('1', 1), ('1.5', 1.5)],
default=1,
render_kw={'group': 'site_info', 'tooltip': 'Use 1.5 for essential facilities such as: Hospitals, Police, Fire & Rescue stations & Designated emergency shelters. All other structures should use 1.0.'})
class ArrayForm(GroupedForm):
file_upload = FileField('System Data (txt)', render_kw={'group': 'array_info', 'class': 'system_upload'})
dxf_upload = FileField('Cad File (dxf)', render_kw={'group': 'dxf_file', 'class': 'system_upload'})
class TestDXFForm(GroupedForm):
dxf_upload = FileField('Cad File (dxf)', render_kw={'group': 'array_info', 'class': 'system_upload'})
show_wind_zones = BooleanField('Show Wind Zones', default=True, render_kw={'group': 'array_info'})

4
helix/functions.py Normal file
View File

@@ -0,0 +1,4 @@
def fequal(x, y, delta=1e-6):
if x == y:
return True
return abs(x - y) < delta

View File

Some files were not shown because too many files have changed in this diff Show More