nickolasclarke / dispatch Goto Github PK
View Code? Open in Web Editor NEWA discrete-event simulation (DES) model for simulating deployment of EV bus fleets, using GTFS data
A discrete-event simulation (DES) model for simulating deployment of EV bus fleets, using GTFS data
placeholder
somewhat the inverse of #21 . If there is a particularly long route, we may get into an infinite loop of creating buses to serve that route.
As first step, we should flag the route and skip. As a second step, we should maybe break the route up? This will be important when we point this at a regional inter-city bus system.
@r-barnes I had a few minutes to move filtering to the parser (and control for all types of non-bus routes), and re-run with some different values based on the email convo. I saw a need for only ~1200 buses with this configuration.
I also sat down and read through the bulk of the documentation for salabim. Aside: some of the most clearly written examples I've seen, really easy to follow and remarkably simple). I now have a better grasp on the mechanics. It seems that currently while we are only using a "centralized" charger, that that charger effectively has an unlimited number places to charge, and is only constrained on total energy available correct? Any bus that is out of power and is returned to the depot begins charging, correct?
If so, my thoughts on next before we can hop on a call with the LBL team:
I'm not quite sure how to tackle the spatial placement of these depots in the sim. Currently the buses dont any sense of space in the sim, In our current arrangement, what are your thoughts on how to approach adding this?
parse_gtfs
is very brittle right now. It essentially only works for Minneapolis. I will work through a battery of other feeds of varying quality to determine where it needs more flexibility. This will likely result in a few other issues, but I'll keep this branch for general work.
Currently we fix the drive train efficiency. We should make more accurate estimates of the route efficiency in both directions of travel based on the elevation profile of the route.
https://github.com/Dungyichao/Electric-Vehicle-Route-Planning-on-Google-Map-Reinforcement-Learning has simple physical model of how this might work.
right now we allow for all the buses in a depot to charge simultaneously. This is likely not accurate.
Expected outcome:
Current outcome:
Further details:
Details about Block_id: 93217702 in AC Transit, scenario 220kwh battery, 500kw nondepot chargers.
See this trips file. Strip the .log filetype after downloading, since GH doesnt permit .csv
Intro:
This block has 3 buses that are used to complete the block: 672, 673, and 674.
First, lets see a summary of all trips in the block. We can see here that we start with bus 672, which leaves from depot 3 and performs a single trip. It then gets reassigned to another block (see details about the bus below) and is replaced by 673. 673 Continues the block until it is out of energy. Once it is out of energy, it returns to depot 3, charges fully (! this is surprising) and then completes one trip. Then Bus 674 takes over, and completing 3 trips and then returning to depot 3. The block is complete.
opti_trips = pd.read_csv('actransit/220kwh_500_kw_trips.csv', index_col=0)
opti_trips[opti_trips['block_id'] == 93217702].sort_values('end_arrival_time')[['bus_id','end_arrival_time','start_arrival_time','start_depot_id','end_depot_id','end_stop_id','start_stop_id','energy_left','distance']]
bus_id | end_arrival_time | start_arrival_time | start_depot_id | end_depot_id | end_stop_id | start_stop_id | energy_left | distance | |
---|---|---|---|---|---|---|---|---|---|
5344 | 672 | 24000 | 21360 | 3 | -9.22337e+18 | 4792 | 4733 | 197.247 | 18629.2 |
5345 | 673 | 27600 | 24900 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 182.63 | 17540.9 |
5346 | 673 | 31440 | 28500 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 167.106 | 18629.2 |
5347 | 673 | 35160 | 32400 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 152.488 | 17540.9 |
5348 | 673 | 38760 | 36000 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 136.964 | 18629.2 |
5349 | 673 | 42360 | 39600 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 122.347 | 17540.9 |
5350 | 673 | 45960 | 43200 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 106.822 | 18629.2 |
5351 | 673 | 49560 | 46800 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 92.2049 | 17540.9 |
5352 | 673 | 53160 | 50400 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 76.6806 | 18629.2 |
5353 | 673 | 57000 | 54000 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 62.0632 | 17540.9 |
5354 | 673 | 60660 | 57900 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 46.5389 | 18629.2 |
5355 | 673 | 64380 | 61380 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 31.9215 | 17540.9 |
5356 | 673 | 68040 | 65400 | -9.22337e+18 | 3 | 4792 | 4733 | 9.46222 | 18629.2 |
5357 | 673 | 71400 | 68700 | 3 | -9.22337e+18 | 4936 | 4221 | 197.414 | 17540.9 |
5358 | 674 | 74940 | 72300 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 181.89 | 18629.2 |
5359 | 674 | 78600 | 75900 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 167.273 | 17540.9 |
5360 | 674 | 82140 | 79500 | -9.22337e+18 | 3 | 4792 | 4733 | 144.813 | 18629.2 |
Next lets look at what each of the buses themselves do. First up, Bus 672:
This bus starts on the block detailed above for one trip. It then gets reassigned to another block, 93217602. (why?) It then serves three trips for that block, completing the block and returning to the depot 3, nearly fully charged.
opti_trips[opti_trips['bus_id'] == 672].sort_values('end_arrival_time')[['block_id', 'end_arrival_time','start_arrival_time','start_depot_id','end_depot_id','end_stop_id','start_stop_id','energy_left','distance']]
block_id | end_arrival_time | start_arrival_time | start_depot_id | end_depot_id | end_stop_id | start_stop_id | energy_left | distance | |
---|---|---|---|---|---|---|---|---|---|
5344 | 9.32177e+07 | 24000 | 21360 | 3 | -9.22337e+18 | 4792 | 4733 | 197.247 | 18629.2 |
5341 | 9.32176e+07 | 73800 | 71100 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 182.63 | 17540.9 |
5342 | 9.32176e+07 | 77640 | 75000 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 167.106 | 18629.2 |
5343 | 9.32176e+07 | 81000 | 78300 | -9.22337e+18 | 3 | 4936 | 4221 | 145.412 | 17540.9 |
Next Bus 673: Since 672 is reassigned, 673 picks up. Since it starts in the middle of the block, it does not have a starting depot listed. It travels to the start of the next trip in the block and continues to serve those trips until it no longer has enough energy to serve the next trip. It returns to depot 3 and charges fully (this is unexpected) then returns to finish the block. I need to dig more to see if it actually meets the required starting time of the next trip in the block, given that it has time to return and fully charge. It then serves one additional trip in the block, then returns to the depot before block has been completed, almost fully charged. It is not deployed again.
opti_trips[opti_trips['bus_id'] == 673].sort_values('end_arrival_time')[['block_id', 'end_arrival_time','start_arrival_time','start_depot_id','end_depot_id','end_stop_id','start_stop_id','energy_left','distance']]
block_id | end_arrival_time | start_arrival_time | start_depot_id | end_depot_id | end_stop_id | start_stop_id | energy_left | distance | |
---|---|---|---|---|---|---|---|---|---|
5345 | 9.32177e+07 | 27600 | 24900 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 182.63 | 17540.9 |
5346 | 9.32177e+07 | 31440 | 28500 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 167.106 | 18629.2 |
5347 | 9.32177e+07 | 35160 | 32400 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 152.488 | 17540.9 |
5348 | 9.32177e+07 | 38760 | 36000 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 136.964 | 18629.2 |
5349 | 9.32177e+07 | 42360 | 39600 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 122.347 | 17540.9 |
5350 | 9.32177e+07 | 45960 | 43200 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 106.822 | 18629.2 |
5351 | 9.32177e+07 | 49560 | 46800 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 92.2049 | 17540.9 |
5352 | 9.32177e+07 | 53160 | 50400 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 76.6806 | 18629.2 |
5353 | 9.32177e+07 | 57000 | 54000 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 62.0632 | 17540.9 |
5354 | 9.32177e+07 | 60660 | 57900 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 46.5389 | 18629.2 |
5355 | 9.32177e+07 | 64380 | 61380 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 31.9215 | 17540.9 |
5356 | 9.32177e+07 | 68040 | 65400 | -9.22337e+18 | 3 | 4792 | 4733 | 9.46222 | 18629.2 |
5357 | 9.32177e+07 | 71400 | 68700 | 3 | -9.22337e+18 | 4936 | 4221 | 197.414 | 17540.9 |
And finally Bus 674:
Since bus 673 returns to the depot, bus 674 is assigned. It, however, does have a starting depot unlike 673 did. (why?) It continues to serve the remaining trips in the block, and then once the block is complete, it returns to the depot.
opti_trips[opti_trips['bus_id'] == 674].sort_values('end_arrival_time')[['block_id', 'end_arrival_time','start_arrival_time','start_depot_id','end_depot_id','end_stop_id','start_stop_id','energy_left','distance']]
block_id | end_arrival_time | start_arrival_time | start_depot_id | end_depot_id | end_stop_id | start_stop_id | energy_left | distance | |
---|---|---|---|---|---|---|---|---|---|
5361 | 9.32178e+07 | 25500 | 22800 | 3 | -9.22337e+18 | 4936 | 4221 | 197.414 | 17540.9 |
5358 | 9.32177e+07 | 74940 | 72300 | -9.22337e+18 | -9.22337e+18 | 4792 | 4733 | 181.89 | 18629.2 |
5359 | 9.32177e+07 | 78600 | 75900 | -9.22337e+18 | -9.22337e+18 | 4936 | 4221 | 167.273 | 17540.9 |
5360 | 9.32177e+07 | 82140 | 79500 | -9.22337e+18 | 3 | 4792 | 4733 | 144.813 | 18629.2 |
A script to download all GTFS feeds from Transit Feeds, validate feed, filter for feeds that include buses, and attempt to run parse_gtfs.py against the feed.
for future.
block_id
is an optional field in the GTFS spec. In order to run against all GTFS feeds, we'll need to refactor to build our own block_id or otherwise on feeds without.
shape_id
We should include additional limits on depth of discharge of the Bus batteries. See M. McCall's EV trucking paper appendix data for more details.
Currently, if you follow the build instructions on Fish shell, it incorrectly parses the pwd
append in the CMAKE
command as part of the build instructions. This leads to a '`pwd' directory and various files missing from the /build/bin
directory
walk_route()
)trip_dist
walk_route
, pull trip.trip_dist
, convert to an energy_req, and subtract from bus['energy']nearby_charger
than check if the time before the next required departure (trip index + 1 in trips
) is less than some threshold. If so, than charge at some given rate self.charge_rate
and append that energy value back to bus['energy']. If there is no nearby charger, than return and the loop will continue, attempting to serve the next trip.Currently we use a fixed charge rate (default of 150 kw) regardless of the battery State of Charge (SoC). This does not match reality and will give us inflated charge values.
Create some pretty pictures and other analysis for each feed's results. Initial work based off analysis I did for PP275.
The following line is should exclusive, instead of inclusive. Not all GTFS feeds will have this same set of columns.
trips = trips.drop(columns=[ . . .
the final pickle still has ~500 trips with RAIL
in the trip_id
string. I'm having trouble determining where they are introduced.
validating on the df at this point suggests that there are no RAIL trips left. However, when I load the final pickle, there are many trips with RAIL
. I also tried searching in the df at line 48 above for one of the integer IDs in the trip_id
string from the final pickle, such as 15269175
from 15269175-AUG19-RAIL-Weekday-01
and its also not present. We seem to be introducing additional trips somewhere.
Basic Costs / Inputs:
Modeling Objective:
Minimize the cost of an EV bus deployment
min
The decisions the model is making, in order to minimize our costs.
These are all discrete integers, but I think that they could be treated as continous and simply round up to the nearest int value.
note: I think all of these could be set as equality constraints instead, and addressed through scenarios
For each feed, the model should be run with a minimum of high/medium/low values for the relevant equality constraints. This would result in a combinations table of all possible scenarios. We can add more nuance to this 3
At the end of each trip, a bus should charge for the time in between the end and start of the next trip - a slight penalty for plugging/unplugging.
found while investigating #89, using the same scenario and data. See that ticket for the csv used.
Expected results:
Observed Results:
In this scenario Bus 767 appears to start with significantly higher power than 220kwh (assuming energy left is in units kwh) It also continues to overcharge, over 2000kwh at its peak. It also serves two trips with the same start/end arrival_time fields, one of which starts from a depot, another of which starts in the middle of a block. This suggests it serves two trips at at time.
opti_trips = pd.read_csv('actransit/220kwh_500_kw_trips.csv', index_col=0)
opti_trips[opti_trips['bus_id'] == 767].sort_values('end_arrival_time')
block_id | bus_busy_end | bus_busy_start | bus_id | distance | end_arrival_time | end_depot_id | end_stop_id | energy_left | start_arrival_time | start_depot_id | start_stop_id | trip_id | wait_time | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
6086 | 9.3227e+07 | 33540 | 30000 | 767 | 22821.2 | 33540 | -9.22337e+18 | 5256 | 621.668 | 30000 | -9.22337e+18 | 5546 | 3.83441e+08 | 1260 |
6098 | 9.3227e+07 | 33540 | 28032.6 | 767 | 22821.2 | 33540 | -9.22337e+18 | 5256 | 358.007 | 30000 | 3 | 5546 | 3.83441e+08 | 1260 |
6087 | 9.3227e+07 | 38280 | 34800 | 767 | 22490 | 38280 | -9.22337e+18 | 1351 | 786.259 | 34800 | -9.22337e+18 | 1593 | 3.83387e+08 | 1320 |
6088 | 9.3227e+07 | 43320 | 39600 | 767 | 22821.2 | 43320 | -9.22337e+18 | 5256 | 917.242 | 39600 | -9.22337e+18 | 5546 | 3.83449e+08 | 1080 |
6089 | 9.3227e+07 | 47940 | 44400 | 767 | 22490 | 47940 | -9.22337e+18 | 1351 | 1073.5 | 44400 | -9.22337e+18 | 1593 | 3.83395e+08 | 1260 |
6090 | 9.3227e+07 | 52980 | 49200 | 767 | 22821.2 | 52980 | -9.22337e+18 | 5256 | 1196.15 | 49200 | -9.22337e+18 | 5546 | 3.83457e+08 | 1020 |
6091 | 9.3227e+07 | 57660 | 54000 | 767 | 22490 | 57660 | -9.22337e+18 | 1351 | 1335.74 | 54000 | -9.22337e+18 | 1593 | 3.83403e+08 | 1140 |
6092 | 9.3227e+07 | 63000 | 58800 | 767 | 22821.2 | 63000 | -9.22337e+18 | 5256 | 1566.72 | 58800 | -9.22337e+18 | 5546 | 3.83465e+08 | 1800 |
6093 | 9.3227e+07 | 68280 | 64800 | 767 | 22490 | 68280 | -9.22337e+18 | 1351 | 1731.31 | 64800 | -9.22337e+18 | 1593 | 3.83412e+08 | 1320 |
6094 | 9.3227e+07 | 73200 | 69600 | 767 | 22821.2 | 73200 | -9.22337e+18 | 5256 | 1878.96 | 69600 | -9.22337e+18 | 5546 | 3.83474e+08 | 1200 |
6095 | 9.3227e+07 | 77520 | 74400 | 767 | 22490 | 77520 | -9.22337e+18 | 1351 | 2093.56 | 74400 | -9.22337e+18 | 1593 | 3.8342e+08 | 1680 |
6096 | 9.3227e+07 | 82800 | 79200 | 767 | 22821.2 | 82800 | -9.22337e+18 | 5256 | 2241.2 | 79200 | -9.22337e+18 | 5546 | 3.83482e+08 | 1200 |
6097 | 9.3227e+07 | 30765.1 | 84000 | 767 | 22490 | 86820 | 3 | 1351 | 2204.82 | 84000 | -9.22337e+18 | 1593 | 3.83428e+08 | 0 |
I noticed that some lines in minneapolis still had the RAIL prefix in their trip title, (15269175-AUG19-RAIL-Weekday-01, which is the 901-112) but are classified as a bus in GTFS. This is because they are trollybuses, with electrified overhead lines. While not strictly necessary, it may be worth adding a filter flag for these in parse_gtfs
in the future.
kwh
of battery capacity
kwh
of battery size, to account for volumetric limitations that would be reached in reality.This should then be used to do a basic fiscal optimization to find the least cost solution for a given transit network. I envision this including kwh
and and level 2 charger penetration (as a function of the total termini in the system) as either tuneable parameters, or inputs into a more traditional optimization cost function.
It seems if we have just a single depot, that a cost minimization optimization should be fairly straightforward? Otherwise, crafting several scenarios of something like high / med / low kwh capacity coupled with high / med / low level-2 charger penetration and brute forcing through a number of scenarios should give a good idea of what is possible without solving the formal optimization.
We currently charge linearly and at full rate from 0% SoC to 100%. This is inaccurate.
somewhat related to #21 and #22. Currently once a block is completed, that bus is no longer considered for use. Currently we assume a block should occupy a bus for an entire day. However, in my cursory analysis of Minneapolis blocks, some of these appear to be rather short, and these buses would be idle.
Furthermore, in my analysis of both the processed GTFS feed dataframes fed into the sim, and the results, it appears that, according to our current calculations, Minneapolis should require ~1554 buses to simply meet all the blocks. But according to Metro Transit they only have 909 buses in their fleet. This further suggests that blocks do not cover a full day. I'll do further analysis of this later.
My initial impressions on how to deal with this would be to be to add some logic in Model.run()
of sim
and run_block()
to release a bus to a queue that can be used by later call of run_block()
, otherwise create a new bus.
I think the parallelism could in Model.run()
could cause some extraneous bus creation. We may sort the blocks by total block duration, instead of earliest start time, but I don't think this covers it entirely still. This parallelism edge case may not be worth considering, given our rough overall accuracy anyhow.
update readme with more detailed install and build instructions.
optimize_bus_distribution.py
currently gives a very low number (99 buses req. to service AC transit, for example)
output when attempting to parse the latest minneapolis gtfs feed.
Traceback (most recent call last):
File "parse_gtfs.py", line 234, in <module>
road_segs, seg_props = GenerateRoadSegments(ptg.load_geo_feed(feed_file))
File "parse_gtfs.py", line 202, in GenerateRoadSegments
shape_props['distance'] = [[SegLength(*x) for x in x] for x in shape_props['seg_hash']]
File "parse_gtfs.py", line 202, in <listcomp>
shape_props['distance'] = [[SegLength(*x) for x in x] for x in shape_props['seg_hash']]
File "parse_gtfs.py", line 202, in <listcomp>
shape_props['distance'] = [[SegLength(*x) for x in x] for x in shape_props['seg_hash']]
File "parse_gtfs.py", line 184, in SegLength
xd = p1[0]-p2[0]
TypeError: 'float' object is not subscriptable
plenty of feeds that otherwise should be supported are not parsed. This is a placeholder to resolve all of these issues.
currently we run each block of trips in series, but they can be run in parallel.
There may be short blocks that take less energy, in total, than a bus starts with. These buses fall through the cracks, I think.
When I run minneaplolis with the default inputs, I encounter many energy traps (where the bus does not have enough energy to return to depot), and seemingly leads to a segfault. I've also bumped up the default value to 300 kwh of battery_cap and it also seg faults. I don't immediately see an issue in the lines that may cause this, they should account for this edge case.
Lines 110 to 128 in af54966
reproduce with the following inputs:
julia --project ./sim.jl ../../data/parsed_minneapolis ../../data/minneapolis-saint-paul_minnesota.osm.pbf ../../data/depots_minneapolis.csv minneapolis_out
Energy trap found!
Energy trap found!
...
Energy trap found!
Energy trap found!
..................
signal (11): Segmentation fault
in expression starting at /mnt/c/Users/nicko/repos/dispatch/build/bin/sim.jl:329
unknown function (ip: 0x7f880c204d17)
unknown function (ip: 0x7f880c205070)
__gxx_personality_v0 at /usr/bin/../lib/x86_64-linux-gnu/libstdc++.so.6 (unknown line)
unknown function (ip: 0x7f880c0006ee)
_Unwind_Resume at /lib/x86_64-linux-gnu/libgcc_s.so.1 (unknown line)
getTravelTime at /home/nclarke/winhome/repos/dispatch/build/../src/routingkit/routingkit.cpp:69
operator() at /home/nclarke/.julia/packages/CxxWrap/lDNAy/deps/usr/include/jlcxx/module.hpp:981 [inlined]
_M_invoke at /usr/include/c++/9/bits/std_function.h:286
operator() at /usr/include/c++/9/bits/std_function.h:688 [inlined]
operator() at /home/nclarke/.julia/packages/CxxWrap/lDNAy/deps/usr/include/jlcxx/module.hpp:47 [inlined]
apply at /home/nclarke/.julia/packages/CxxWrap/lDNAy/deps/usr/include/jlcxx/module.hpp:72
getTravelTime at /home/nclarke/.julia/packages/CxxWrap/lDNAy/src/CxxWrap.jl:552
TimeDistanceBetweenPoints at /mnt/c/Users/nicko/repos/dispatch/build/bin/sim.jl:53
FindClosestDepotByTime at /mnt/c/Users/nicko/repos/dispatch/build/bin/sim.jl:230
RunBlock at /mnt/c/Users/nicko/repos/dispatch/build/bin/sim.jl:84
Model at /mnt/c/Users/nicko/repos/dispatch/build/bin/sim.jl:201
main at /mnt/c/Users/nicko/repos/dispatch/build/bin/sim.jl:310
unknown function (ip: 0x7f881048ef7c)
unknown function (ip: 0x7f881048ec49)
unknown function (ip: 0x7f881048f0b0)
unknown function (ip: 0x7f881048f668)
unknown function (ip: 0x7f881048fe8f)
unknown function (ip: 0xfffffffffffffffe)
unknown function (ip: 0x7f87dda9a08f)
unknown function (ip: 0xffffffffffffffff)
unknown function (ip: 0x7f881049025e)
unknown function (ip: 0x7f8810370276)
unknown function (ip: 0x7f881034e7ad)
jl_load at /usr/bin/../lib/x86_64-linux-gnu/libjulia.so.1 (unknown line)
include at ./boot.jl:328 [inlined]
include_relative at ./loading.jl:1105
include at ./Base.jl:31
exec_options at ./client.jl:287
_start at ./client.jl:460
jfptr__start_2084.clone_1 at /usr/lib/x86_64-linux-gnu/julia/sys.so (unknown line)
unknown function (ip: 0x7f8810862755)
unknown function (ip: 0x7f8810862332)
__libc_start_main at /lib/x86_64-linux-gnu/libc.so.6 (unknown line)
unknown function (ip: 0x7f88108623d9)
Allocations: 172195181 (Pool: 172176543; Big: 18638); GC: 141
fish: “julia --project ./sim.jl ../../…” terminated by signal SIGSEGV (Address boundary error)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.