first commit
4
.babelrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["stage-2", "es2015" ],
|
||||
"plugins": ["transform-object-assign"]
|
||||
}
|
||||
10
.cfignore
Normal file
@@ -0,0 +1,10 @@
|
||||
env
|
||||
__pycache__
|
||||
Gemfile*
|
||||
node_modules
|
||||
test
|
||||
spec
|
||||
lib
|
||||
package.json
|
||||
npm-shrinkwrap.json
|
||||
Rakefile
|
||||
23
.editorconfig
Normal 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
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
env
|
||||
.idea
|
||||
__pycache__
|
||||
.DS_Store
|
||||
*.log
|
||||
*cache
|
||||
.js.map
|
||||
80
Dockerfile
Normal 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
@@ -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
@@ -0,0 +1,4 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'sass'
|
||||
gem 'pivotal-tracker'
|
||||
46
Gemfile.lock
Normal 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
|
||||
286
README.md
Normal 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`.
|
||||
|
||||

|
||||
|
||||
#### Configuring targets
|
||||
|
||||
For this, we're going to reference the target configuration menu on pycharm (this is the top-right triangle-esque menu)
|
||||
|
||||

|
||||
|
||||
##### 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!).
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
##### 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`. 
|
||||
|
||||
#### 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`
|
||||
|
||||

|
||||
|
||||
It's necessary to set path mappings when configuring targets:
|
||||
|
||||

|
||||
|
||||
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
@@ -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
@@ -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
@@ -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
|
||||
142
db/env.py
Normal 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
@@ -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"}
|
||||
64
db/versions/00817cda9d17_add_ebom_tables.py
Normal 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)
|
||||
@@ -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')
|
||||
37
db/versions/3cb6ab91fdc2_update_inverter_models_enum.py
Normal 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
|
||||
28
db/versions/72342d883290_add_file_names_to_site.py
Normal 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')
|
||||
56
db/versions/a904d0d1e1a7_create_initial_tables.py
Normal 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)
|
||||
|
||||
39
db/versions/ed4c4bd22d6a_add_cascade_delete_for_inverter.py
Normal 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']
|
||||
)
|
||||
30
db/versions/f90d04c490dc_add_inverter_brand_table.py
Normal 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
@@ -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
|
||||
BIN
documentation/pycharm_docker.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
documentation/pycharm_docker_config.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
documentation/setup_pycharm_alltests_target.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
documentation/setup_pycharm_correct_python.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
documentation/setup_pycharm_server_target.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
documentation/setup_pycharm_target_configuration.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
documentation/setup_pycharm_unittests_configuration.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
0
helix/Repositories/__init__.py
Normal file
42
helix/Repositories/graph_repository.py
Normal 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])
|
||||
0
helix/Services/__init__.py
Normal file
22
helix/Services/doc_gen_service.py
Normal 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
94
helix/Services/dxf_service.py
Normal 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
1
helix/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__author__ = 'pivotal'
|
||||
65
helix/api/api.py
Normal 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
|
||||
})
|
||||
78
helix/calculated_data_repository.py
Normal 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
|
||||
0
helix/calculators/__init__.py
Normal file
248
helix/calculators/ballast_calculator.py
Normal 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
|
||||
58
helix/calculators/bom_calculator.py
Normal 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
|
||||
36
helix/calculators/bom_helper.py
Normal 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
|
||||
169
helix/calculators/calculator.py
Normal 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)
|
||||
104
helix/calculators/coordinates_calculator.py
Normal 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))
|
||||
|
||||
|
||||
|
||||
133
helix/calculators/ebom_calculator.py
Normal 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
|
||||
148
helix/calculators/mechanical_bom_calculator.py
Normal 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
|
||||
|
||||
93
helix/calculators/pressure_coefficient_calculator.py
Normal 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
|
||||
184
helix/calculators/seismic_calculator.py
Normal 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)
|
||||
246
helix/calculators/subarray_graph.py
Normal 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
|
||||
19
helix/calculators/subarray_helper.py
Normal 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]
|
||||
85
helix/calculators/summary_values_calculator.py
Normal 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
|
||||
56
helix/calculators/wind_pressure_calculator.py
Normal 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)
|
||||
0
helix/constants/__init__.py
Normal file
23
helix/constants/anchor_parts.py
Normal 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
|
||||
}
|
||||
29
helix/constants/anchor_type.py
Normal 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
|
||||
4
helix/constants/bom_constants.py
Normal 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
@@ -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"
|
||||
89
helix/constants/dual_tilt_parts.py
Normal 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,
|
||||
}
|
||||
7
helix/constants/dxf_validation.py
Normal 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."
|
||||
281
helix/constants/ebom_parts.py
Normal 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,
|
||||
}
|
||||
}
|
||||
34
helix/constants/exposure_category.py
Normal 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
|
||||
50
helix/constants/file_validation_error.py
Normal 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
|
||||
9
helix/constants/global_constants.py
Normal 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
|
||||
25
helix/constants/inverter_brand.py
Normal 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}
|
||||
94
helix/constants/inverter_type.py
Normal 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()
|
||||
11
helix/constants/module_type.py
Normal 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
|
||||
0
helix/constants/module_type_constants/__init__.py
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
46
helix/constants/panel_type.py
Normal 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
@@ -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
|
||||
]
|
||||
7
helix/constants/redis_constant.py
Normal 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)
|
||||
5
helix/constants/seismic_anchor_validation_error.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class SeismicAnchorValidationError(Enum):
|
||||
TooFewAnchors = 'There are too few anchors in one or more subarrays'
|
||||
94
helix/constants/single_tilt_parts.py
Normal 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
|
||||
}
|
||||
6
helix/constants/sql_constant.py
Normal 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'))
|
||||
6
helix/constants/subarray.py
Normal 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'."
|
||||
60
helix/constants/system_type.py
Normal 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]
|
||||
0
helix/constants/system_type_constants/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
class DualTiltConstants(object):
|
||||
wind_zones = ['A', 'B', 'C', 'D', 'E']
|
||||
module_count = 2
|
||||
minimum_corner_module_count = 2
|
||||
@@ -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
|
||||
7
helix/constants/version.py
Normal 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
@@ -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
17
helix/db/redis_manager.py
Normal 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
@@ -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)
|
||||
166
helix/doc_gen_params_builder.py
Normal 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
17
helix/forms/conditional_validator.py
Normal 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
@@ -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)
|
||||
6
helix/forms/grouped_form.py
Normal 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
@@ -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
@@ -0,0 +1,4 @@
|
||||
def fequal(x, y, delta=1e-6):
|
||||
if x == y:
|
||||
return True
|
||||
return abs(x - y) < delta
|
||||